Add Markdown support

This commit is contained in:
Nate Moore 2021-09-17 21:22:51 +00:00 committed by Matthew Phillips
parent 3df506a219
commit 5909f27b19
43 changed files with 1035 additions and 808 deletions

View file

@ -0,0 +1,9 @@
<h1>
<slot/>
</h1>
<style>
h1 {
color: red;
}
</style>

View file

@ -1,12 +1,12 @@
---
import { Markdown } from 'astro/components';
import BaseHead from '../components/BaseHead.astro';
import BlogHeader from '../components/BlogHeader.astro';
import BlogPost from '../components/BlogPost.astro';
// import BlogHeader from '../components/BlogHeader.astro';
// import BlogPost from '../components/BlogPost.astro';
const {content} = Astro.props;
const {title, description, publishDate, author, heroImage, permalink, alt} = content;
---
<html lang={ content.lang || 'en' }>
<head>
<BaseHead title={title} description={description} permalink={permalink} />
@ -14,10 +14,13 @@ const {title, description, publishDate, author, heroImage, permalink, alt} = con
</head>
<body>
<BlogHeader />
<BlogPost title={title} author={author} heroImage={heroImage} publishDate={publishDate} alt={alt}>
<h1>Hello world!</h1>
<div class="container">
<slot />
</BlogPost>
</div>
<!-- <BlogHeader /> -->
<!-- <BlogPost title={title} author={author} heroImage={heroImage} publishDate={publishDate} alt={alt}> -->
<!-- </BlogPost> -->
</body>
</html>

View file

@ -18,7 +18,7 @@ let permalink = 'https://example.com/';
// Data Fetching: List all Markdown posts in the repo.
let allPosts = Astro.fetchContent<MarkdownFrontmatter>('./posts/*.md');
let allPosts = await Astro.fetchContent('./posts/*.md');
allPosts = allPosts.sort((a, b) => new Date(b.publishDate).valueOf() - new Date(a.publishDate).valueOf());
// Full Astro Component Syntax:

View file

@ -0,0 +1,15 @@
---
setup: |
import Layout from '../../layouts/BlogPost.astro'
import Cool from '../../components/Author.astro'
name: Nate Moore
value: 128
---
# Hello world!
<Cool name={frontmatter.name} href="https://twitter.com/n_moore" client:load />
This is so cool!
Do variables work {frontmatter.value * 2}?

View file

@ -12,11 +12,11 @@
"build": "yarn build:core",
"build:one": "lerna run build --scope",
"build:all": "lerna run build --scope \"{astro,@astrojs/*}\"",
"build:core": "lerna run build --scope \"{astro,@astrojs/parser,@astrojs/markdown-support}\"",
"build:core": "lerna run build --scope \"{astro,@astrojs/parser,@astrojs/markdown-remark}\"",
"dev": "yarn dev:core --parallel --stream",
"dev:one": "lerna run dev --scope --parallel --stream",
"dev:all": "lerna run dev --scope \"{astro,@astrojs/*}\" --parallel --stream",
"dev:core": "lerna run dev --scope \"{astro,@astrojs/parser,@astrojs/markdown-support}\" --parallel --stream",
"dev:core": "lerna run dev --scope \"{astro,@astrojs/parser,@astrojs/markdown-remark}\" --parallel --stream",
"format": "prettier -w .",
"lint": "eslint \"packages/**/*.ts\"",
"test": "yarn workspace astro run test",
@ -24,6 +24,7 @@
},
"workspaces": [
"compiled/*",
"packages/markdown/*",
"packages/renderers/*",
"packages/*",
"examples/*",
@ -45,6 +46,7 @@
"devDependencies": {
"@changesets/cli": "^2.16.0",
"@types/jest": "^27.0.1",
"@octokit/action": "^3.15.4",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.18.0",
"autoprefixer": "^10.2.6",

View file

@ -1,5 +1,26 @@
---
import { renderMarkdown } from '@astrojs/markdown-support';
import { renderMarkdown } from '@astrojs/markdown-remark';
export interface Props {
content?: string;
}
// Internal props that should not be part of the external interface.
interface InternalProps extends Props {
$scope: string;
}
const __TopLevelAstro = {
site: new URL("http://localhost:3000"),
fetchContent: (globResult) => fetchContent(globResult, import.meta.url),
resolve(...segments) {
return segments.reduce(
(url, segment) => new URL(segment, url),
new URL("http://localhost:3000/packages/astro/components/Markdown.astro")
).pathname
},
};
const Astro = __TopLevelAstro;
export interface Props {
content?: string;

View file

@ -39,8 +39,8 @@
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
},
"dependencies": {
"@astrojs/compiler": "^0.1.0-canary.36",
"@astrojs/markdown-support": "^0.3.1",
"@astrojs/compiler": "^0.1.0-canary.37",
"@astrojs/markdown-remark": "^0.3.1",
"@babel/core": "^7.15.0",
"@web/rollup-plugin-html": "^1.9.1",
"astring": "^1.7.5",

View file

@ -1,12 +1,9 @@
import type { AstroMarkdownOptions } from '@astrojs/markdown-support';
import type babel from '@babel/core';
import type vite from 'vite';
import type { z } from 'zod';
import type { AstroConfigSchema } from '../config';
import type { AstroComponentFactory } from '../internal';
export { AstroMarkdownOptions };
export interface AstroComponentMetadata {
displayName: string;
hydrate?: 'load' | 'idle' | 'visible' | 'media' | 'only';
@ -59,7 +56,9 @@ export interface AstroUserConfig {
*/
renderers?: string[];
/** Options for rendering markdown content */
markdownOptions?: Partial<AstroMarkdownOptions>;
markdownOptions?: {
render?: [string, Record<string, any>];
};
/** Options specific to `astro build` */
buildOptions?: {
/** Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs. */
@ -103,9 +102,7 @@ export interface AstroUserConfig {
// export interface AstroUserConfig extends z.input<typeof AstroConfigSchema> {
// markdownOptions?: Partial<AstroMarkdownOptions>;
// }
export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
markdownOptions: Partial<AstroMarkdownOptions>;
}
export type AstroConfig = z.output<typeof AstroConfigSchema>;
export type AsyncRendererComponentFn<U> = (Component: any, props: any, children: string | undefined, metadata?: AstroComponentMetadata) => Promise<U>;

View file

@ -1,95 +0,0 @@
import type { AstroMarkdownOptions } from '@astrojs/markdown-support';
import type { AstroConfigSchema } from '../config';
import type { z } from 'zod';
/**
* The Astro User Config Format:
* This is the type interface for your astro.config.mjs default export.
*/
export interface AstroUserConfig {
/**
* Where to resolve all URLs relative to. Useful if you have a monorepo project.
* Default: '.' (current working directory)
*/
projectRoot?: string;
/**
* Path to the `astro build` output.
* Default: './dist'
*/
dist?: string;
/**
* Path to all of your Astro components, pages, and data.
* Default: './src'
*/
src?: string;
/**
* Path to your Astro/Markdown pages. Each file in this directory
* becomes a page in your final build.
* Default: './src/pages'
*/
pages?: string;
/**
* Path to your public files. These are copied over into your build directory, untouched.
* Useful for favicons, images, and other files that don't need processing.
* Default: './public'
*/
public?: string;
/**
* Framework component renderers enable UI framework rendering (static and dynamic).
* When you define this in your configuration, all other defaults are disabled.
* Default: [
* '@astrojs/renderer-svelte',
* '@astrojs/renderer-vue',
* '@astrojs/renderer-react',
* '@astrojs/renderer-preact',
* ],
*/
renderers?: string[];
/** Options for rendering markdown content */
markdownOptions?: Partial<AstroMarkdownOptions>;
/** Options specific to `astro build` */
buildOptions?: {
/** Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs. */
site?: string;
/** Generate an automatically-generated sitemap for your build.
* Default: true
*/
sitemap?: boolean;
/**
* Control the output file URL format of each page.
* If 'file', Astro will generate a matching HTML file (ex: "/foo.html") instead of a directory.
* If 'directory', Astro will generate a directory with a nested index.html (ex: "/foo/index.html") for each page.
* Default: 'directory'
*/
pageUrlFormat?: 'file' | 'directory';
};
/** Options for the development server run with `astro dev`. */
devOptions?: {
hostname?: string;
/** The port to run the dev server on. */
port?: number;
/** Path to tailwind.config.js, if used */
tailwindConfig?: string;
/**
* Configure The trailing slash behavior of URL route matching:
* 'always' - Only match URLs that include a trailing slash (ex: "/foo/")
* 'never' - Never match URLs that include a trailing slash (ex: "/foo")
* 'ignore' - Match URLs regardless of whether a trailing "/" exists
* Default: 'always'
*/
trailingSlash?: 'always' | 'never' | 'ignore';
};
}
// NOTE(fks): We choose to keep our hand-generated AstroUserConfig interface so that
// we can add JSDoc-style documentation and link to the definition file in our repo.
// However, Zod comes with the ability to auto-generate AstroConfig from the schema
// above. If we ever get to the point where we no longer need the dedicated
// @types/config.ts file, consider replacing it with the following lines:
//
// export interface AstroUserConfig extends z.input<typeof AstroConfigSchema> {
// markdownOptions?: Partial<AstroMarkdownOptions>;
// }
export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
markdownOptions: Partial<AstroMarkdownOptions>;
}

View file

@ -1 +1 @@
export { AstroConfig, AstroUserConfig } from './config';
export { AstroConfig, AstroUserConfig } from './astro';

View file

@ -1,4 +1,4 @@
import type { AstroConfig } from './@types/astro';
import type { AstroConfig, AstroUserConfig } from './@types/astro';
import { existsSync } from 'fs';
import getPort from 'get-port';
@ -6,7 +6,6 @@ import * as colors from 'kleur/colors';
import path from 'path';
import { pathToFileURL } from 'url';
import { z } from 'zod';
import { AstroUserConfig } from './@types/config';
export const AstroConfigSchema = z.object({
projectRoot: z
@ -41,6 +40,7 @@ export const AstroConfigSchema = z.object({
gfm: z.boolean().optional(),
remarkPlugins: z.array(z.any()).optional(),
rehypePlugins: z.array(z.any()).optional(),
render: z.any().optional().default(['@astrojs/markdown-remark', {}]),
})
.optional()
.default({}),

View file

@ -12,6 +12,8 @@ import { performance } from 'perf_hooks';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
import stripAnsi from 'strip-ansi';
import path from 'path';
import { promises as fs } from 'fs';
import vite from 'vite';
import { defaultLogOptions, error, info } from '../logger.js';
import { createRouteManifest, matchRoute } from '../runtime/routing.js';

View file

@ -3,9 +3,10 @@ import type { AstroComponentMetadata } from '../@types/astro';
import { valueToEstree, Value } from 'estree-util-value-to-estree';
import * as astring from 'astring';
import shorthash from 'shorthash';
import { renderAstroComponent } from '../runtime/astro.js';
import { renderToString, renderAstroComponent } from '../runtime/astro.js';
const { generate, GENERATOR } = astring;
// A more robust version alternative to `JSON.stringify` that can handle most values
// see https://github.com/remcohaszing/estree-util-value-to-estree#readme
const customGenerator: astring.Generator = {
@ -25,16 +26,21 @@ const serialize = (value: Value) =>
generator: customGenerator,
});
async function _render(child: any) {
async function _render(child: any): Promise<any> {
child = await child;
if (Array.isArray(child)) {
return (await Promise.all(child.map((value) => _render(value)))).join('\n');
} else if (typeof child === 'function') {
// Special: If a child is a function, call it automatically.
// This lets you do {() => ...} without the extra boilerplate
// of wrapping it in a function and calling it.
if (typeof child === 'function') {
return await child();
} else if (typeof child === 'string') {
return child;
} else if (!child && child !== 0) {
// do nothing, safe to ignore falsey values.
} else if (child instanceof AstroComponent) {
return await renderAstroComponent(child);
} else {
return child;
}
@ -43,7 +49,6 @@ async function _render(child: any) {
export class AstroComponent {
private htmlParts: string[];
private expressions: TemplateStringsArray;
constructor(htmlParts: string[], expressions: TemplateStringsArray) {
this.htmlParts = htmlParts;
this.expressions = expressions;
@ -129,12 +134,20 @@ setup("${astroId}", {${metadata.hydrateArgs ? `value: ${JSON.stringify(metadata.
return hydrationScript;
}
export const renderComponent = async (result: any, displayName: string, Component: unknown, _props: Record<string | number, any>, children: any) => {
export const renderSlot = async (result: any, slotted: string, fallback?: any) => {
if (slotted) {
return _render(slotted);
}
return fallback;
};
export const renderComponent = async (result: any, displayName: string, Component: unknown, _props: Record<string | number, any>, slots?: any) => {
Component = await Component;
// children = await renderGenerator(children);
const { renderers } = result._metadata;
if (Component && (Component as any).isAstroComponentFactory) {
const output = await renderAstroComponent(await (Component as any)(result, Component, _props, children));
const output = await renderToString(result, Component as any, _props, slots);
return output;
}

View file

@ -1,4 +1,4 @@
import '@vite/client';
import '/@vite/client';
if (import.meta.hot) {
const parser = new DOMParser();

View file

@ -1,15 +0,0 @@
import type { SourceDescription } from 'rollup';
import { renderMarkdownWithFrontmatter } from '@astrojs/markdown-support';
import astroParser from '@astrojs/parser';
import { SourceMapGenerator } from 'source-map';
/** transform .md contents into Astro h() function */
export async function markdownToH(filename: string, contents: string): Promise<SourceDescription> {
const { astro, content } = await renderMarkdownWithFrontmatter(contents);
const map = new SourceMapGenerator();
return {
code: content,
map: null,
};
}

View file

@ -2,6 +2,7 @@ import type { BuildResult } from 'esbuild';
import type { ViteDevServer } from 'vite';
import type { AstroConfig, ComponentInstance, GetStaticPathsResult, Params, Props, RouteCache, RouteData, RuntimeMode, SSRError } from '../@types/astro';
import type { LogOptions } from '../logger';
import type { PathsOutput } from 'fdir';
import cheerio from 'cheerio';
import * as eslexer from 'es-module-lexer';
@ -9,6 +10,7 @@ import { fileURLToPath } from 'url';
import fs from 'fs';
import path from 'path';
import { renderPage } from './astro.js';
import { fdir } from 'fdir';
import { generatePaginateFunction } from './paginate.js';
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
import { parseNpmName, canonicalURL as getCanonicalURL, codeFrame } from './util.js';
@ -95,7 +97,6 @@ async function resolveImportedModules(viteServer: ViteDevServer, file: string) {
let importedModules: Record<string, any> = {};
const moduleNodes = Array.from(modulesByFile);
// Loop over the importedModules and grab the exports from each one.
// We'll pass these to the shared $$result so renderers can match
// components to their exported identifier and URL
@ -173,26 +174,56 @@ export async function ssr({ astroConfig, filePath, logging, mode, origin, pathna
const fullURL = new URL(pathname, origin);
const Component = await mod.default;
if (!Component) throw new Error(`Expected an exported Astro component but recieved typeof ${typeof Component}`);
const ext = path.posix.extname(filePath.pathname);
if (!Component)
throw new Error(`Expected an exported Astro component but recieved typeof ${typeof Component}`);
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
let html = await renderPage(
{
const result = {
styles: new Set(),
scripts: new Set(),
/** This function returns the `Astro` faux-global */
createAstro(props: any) {
createAstro: (props: any) => {
const site = new URL(origin);
const url = new URL('.' + pathname, site);
const canonicalURL = getCanonicalURL(pathname, astroConfig.buildOptions.site || origin);
return { isPage: true, site, request: { url, canonicalURL }, props };
const canonicalURL = getCanonicalURL(pathname, astroConfig.buildOptions.site || origin)
const fetchContent = createFetchContent(fileURLToPath(filePath));
return {
isPage: true,
site,
request: { url, canonicalURL },
props,
fetchContent
};
},
_metadata: { importedModules, renderers },
}
const createFetchContent = (currentFilePath: string) => {
return async (pattern: string) => {
const cwd = path.dirname(currentFilePath);
const crawler = new fdir().glob(pattern);
const files = await crawler.crawlWithOptions(cwd, {
resolvePaths: true,
includeBasePath: true,
filters: [(p) => p !== currentFilePath]
}).withPromise() as PathsOutput;
const contents = await Promise.all(files.map(async file => {
const { default: ChildComponent } = (await viteServer.ssrLoadModule(file)) as ComponentInstance;
return renderPage({
...result,
createAstro: (props: any) => {
return { props }
},
Component,
{},
null
);
}, ChildComponent, {}, null);
}))
return contents;
}
}
let html = await renderPage(result, Component, {}, null);
// 4. modify response
if (mode === 'development') {

View file

@ -9,6 +9,7 @@ import { fileURLToPath } from 'url';
import { createRequire } from 'module';
import { getPackageJSON, parseNpmName } from '../util.js';
import astro from './plugin-astro.js';
import markdown from './plugin-markdown.js';
import jsx from './plugin-jsx.js';
import { AstroDevServer } from '../../dev';
@ -34,6 +35,7 @@ export async function loadViteConfig(
});
const userDevDeps = Object.keys(packageJSON?.devDependencies || {});
const { external, noExternal } = await viteSSRDeps([...userDeps, ...userDevDeps]);
// console.log(external.has('tiny-glob'), noExternal.has('tiny-glob'));
// load Astro renderers
await Promise.all(
@ -78,7 +80,7 @@ export async function loadViteConfig(
/** Always include these dependencies for optimization */
include: [...optimizedDeps],
},
plugins: [astro({ config: astroConfig, devServer }), jsx({ config: astroConfig, logging }), ...plugins],
plugins: [astro({ config: astroConfig, devServer }), markdown({ config: astroConfig, devServer }), jsx({ config: astroConfig, logging }), ...plugins],
publicDir: fileURLToPath(astroConfig.public),
resolve: {
dedupe: [...dedupe],

View file

@ -1,10 +1,11 @@
import type { TransformResult } from '@astrojs/compiler';
import type { Plugin } from 'vite';
import type { AstroConfig, Renderer } from '../../@types/astro.js';
import type { LogOptions } from '../../logger';
import esbuild from 'esbuild';
import fs from 'fs';
import { transform } from '@astrojs/compiler';
import { decode } from 'sourcemap-codec';
import { AstroDevServer } from '../../dev/index.js';
interface AstroPluginOptions {
@ -13,18 +14,21 @@ interface AstroPluginOptions {
}
/** Transform .astro files for Vite */
export default function astro({ devServer }: AstroPluginOptions): Plugin {
export default function astro({ config, devServer }: AstroPluginOptions): Plugin {
return {
name: '@astrojs/vite-plugin-astro',
enforce: 'pre', // run transforms before other plugins can
// note: dont claim .astro files with resolveId() — it prevents Vite from transpiling the final JS (import.meta.globEager, etc.)
async load(id) {
if (id.endsWith('.astro') || id.endsWith('.md')) {
if (id.endsWith('.astro')) {
// const isPage = id.startsWith(fileURLToPath(config.pages));
let source = await fs.promises.readFile(id, 'utf8');
let tsResult: TransformResult | undefined;
try {
// 1. Transform from `.astro` to valid `.ts`
// use `sourcemap: "inline"` so that the sourcemap is included in the "code" result that we pass to esbuild.
const tsResult = await transform(source, { sourcefile: id, sourcemap: 'inline' });
tsResult = await transform(source, { sourcefile: id, sourcemap: 'inline', internalURL: 'astro/internal' });
// 2. Compile `.ts` to `.js`
const { code, map } = await esbuild.transform(tsResult.code, { loader: 'ts', sourcemap: 'inline', sourcefile: id });
@ -32,14 +36,20 @@ export default function astro({ devServer }: AstroPluginOptions): Plugin {
code,
map,
};
} catch (err: any) {
// if esbuild threw the error, find original code source to display
if (err.errors) {
const sourcemapb64 = (tsResult?.code.match(/^\/\/# sourceMappingURL=data:application\/json;charset=utf-8;base64,(.*)/m) || [])[1];
if (!sourcemapb64) throw err;
const json = JSON.parse(new Buffer(sourcemapb64, 'base64').toString());
const mappings = decode(json.mappings);
const focusMapping = mappings[err.errors[0].location.line + 1];
err.sourceLoc = { file: id, line: (focusMapping[0][2] || 0) + 1, column: (focusMapping[0][3] || 0) + 1 };
}
throw err;
}
}
// UNCOMMENT WHEN MARKDOWN SUPPORT LANDS
// } else if (id.endsWith('.md')) {
// let contents = await fs.promises.readFile(id, 'utf8');
// const filename = slash(id.replace(fileURLToPath(config.projectRoot), ''));
// return markdownToH(filename, contents);
// }
return null;
},
async handleHotUpdate(context) {

View file

@ -0,0 +1,68 @@
import type { Plugin } from 'vite';
import type { AstroConfig, Renderer } from '../../@types/astro.js';
import esbuild from 'esbuild';
import fs from 'fs';
import { transform } from '@astrojs/compiler';
import { AstroDevServer } from '../../dev/index.js';
interface AstroPluginOptions {
config: AstroConfig;
devServer?: AstroDevServer;
}
/** Transform .astro files for Vite */
export default function markdown({ config }: AstroPluginOptions): Plugin {
return {
name: '@astrojs/vite-plugin-markdown',
enforce: 'pre', // run transforms before other plugins can
async load(id) {
if (id.endsWith('.md')) {
let source = await fs.promises.readFile(id, 'utf8');
// 2. Transform from `.md` to valid `.astro`
let render = config.markdownOptions.render;
let renderOpts = {};
if (Array.isArray(render)) {
render = render[0];
renderOpts = render[1];
}
if (typeof render === 'string') {
({ default: render } = await import(render));
}
let { frontmatter, metadata, code: astroResult } = await render(source, renderOpts);
// Extract special frontmatter keys
const { layout = '', components = '', setup = '', ...content } = frontmatter;
const prelude = `---
${layout ? `import Layout from '${layout}';` : ''}
${components ? `import * from '${components}';` : ''}
${setup}
---`;
// If the user imported "Layout", wrap the content in a Layout
if (/\bLayout\b/.test(prelude)) {
astroResult = `${prelude}\n<Layout content={${JSON.stringify(content)}}>\n\n${astroResult}\n\n</Layout>`;
} else {
astroResult = `${prelude}\n${astroResult}`;
}
// 2. Transform from `.astro` to valid `.ts`
let { code: tsResult } = await transform(astroResult, { sourcefile: id, sourcemap: 'inline', internalURL: 'astro/internal' });
tsResult = `\nexport const metadata = ${JSON.stringify(metadata)};
export const frontmatter = ${JSON.stringify(content)};
${tsResult}`;
// 3. Compile `.ts` to `.js`
const { code, map } = await esbuild.transform(tsResult, { loader: 'ts', sourcemap: 'inline', sourcefile: id });
return {
code,
map: null,
};
}
return null;
},
};
}

View file

@ -1,229 +0,0 @@
/**
* The MIT License (MIT)
*
* Copyright (c) 2014-2018, Jon Schlinkert.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
'use strict';
import sections from 'section-matter';
import defaults from './lib/defaults.js';
import stringify from './lib/stringify.js';
import excerpt from './lib/excerpt.js';
import engines from './lib/engines.js';
import toFile from './lib/to-file.js';
import parse from './lib/parse.js';
import * as utils from './lib/utils.js';
/**
* Takes a string or object with `content` property, extracts
* and parses front-matter from the string, then returns an object
* with `data`, `content` and other [useful properties](#returned-object).
*
* ```js
* const matter = require('gray-matter');
* console.log(matter('---\ntitle: Home\n---\nOther stuff'));
* //=> { data: { title: 'Home'}, content: 'Other stuff' }
* ```
* @param {Object|String} `input` String, or object with `content` string
* @param {Object=} `options`
* @return {{content: string, data: Record<string, string>}}
* @api public
*/
function matter(input, options) {
if (input === '') {
return { data: {}, content: input, excerpt: '', orig: input };
}
let file = toFile(input);
const cached = matter.cache[file.content];
if (!options) {
if (cached) {
file = Object.assign({}, cached);
file.orig = cached.orig;
return file;
}
// only cache if there are no options passed. if we cache when options
// are passed, we would need to also cache options values, which would
// negate any performance benefits of caching
matter.cache[file.content] = file;
}
return parseMatter(file, options);
}
/**
* Parse front matter
*/
function parseMatter(file, options) {
const opts = defaults(options);
const open = opts.delimiters[0];
const close = '\n' + opts.delimiters[1];
let str = file.content;
if (opts.language) {
file.language = opts.language;
}
// get the length of the opening delimiter
const openLen = open.length;
if (!utils.startsWith(str, open, openLen)) {
excerpt(file, opts);
return file;
}
// if the next character after the opening delimiter is
// a character from the delimiter, then it's not a front-
// matter delimiter
if (str.charAt(openLen) === open.slice(-1)) {
return file;
}
// strip the opening delimiter
str = str.slice(openLen);
const len = str.length;
// use the language defined after first delimiter, if it exists
const language = matter.language(str, opts);
if (language.name) {
file.language = language.name;
str = str.slice(language.raw.length);
}
// get the index of the closing delimiter
let closeIndex = str.indexOf(close);
if (closeIndex === -1) {
closeIndex = len;
}
// get the raw front-matter block
file.matter = str.slice(0, closeIndex);
const block = file.matter.replace(/^\s*#[^\n]+/gm, '').trim();
if (block === '') {
file.isEmpty = true;
file.empty = file.content;
file.data = {};
} else {
// create file.data by parsing the raw file.matter block
file.data = parse(file.language, file.matter, opts);
}
// update file.content
if (closeIndex === len) {
file.content = '';
} else {
file.content = str.slice(closeIndex + close.length);
if (file.content[0] === '\r') {
file.content = file.content.slice(1);
}
if (file.content[0] === '\n') {
file.content = file.content.slice(1);
}
}
excerpt(file, opts);
if (opts.sections === true || typeof opts.section === 'function') {
sections(file, opts.section);
}
return file;
}
/**
* Expose engines
*/
matter.engines = engines;
/**
* Stringify an object to YAML or the specified language, and
* append it to the given string. By default, only YAML and JSON
* can be stringified. See the [engines](#engines) section to learn
* how to stringify other languages.
*
* ```js
* console.log(matter.stringify('foo bar baz', {title: 'Home'}));
* // results in:
* // ---
* // title: Home
* // ---
* // foo bar baz
* ```
* @param {String|Object} `file` The content string to append to stringified front-matter, or a file object with `file.content` string.
* @param {Object} `data` Front matter to stringify.
* @param {Object} `options` [Options](#options) to pass to gray-matter and [js-yaml].
* @return {String} Returns a string created by wrapping stringified yaml with delimiters, and appending that to the given string.
* @api public
*/
matter.stringify = function (file, data, options) {
if (typeof file === 'string') file = matter(file, options);
return stringify(file, data, options);
};
/**
* Returns true if the given `string` has front matter.
* @param {String} `string`
* @param {Object} `options`
* @return {Boolean} True if front matter exists.
* @api public
*/
matter.test = function (str, options) {
return utils.startsWith(str, defaults(options).delimiters[0]);
};
/**
* Detect the language to use, if one is defined after the
* first front-matter delimiter.
* @param {String} `string`
* @param {Object} `options`
* @return {Object} Object with `raw` (actual language string), and `name`, the language with whitespace trimmed
*/
matter.language = function (str, options) {
const opts = defaults(options);
const open = opts.delimiters[0];
if (matter.test(str)) {
str = str.slice(open.length);
}
const language = str.slice(0, str.search(/\r?\n/));
return {
raw: language,
name: language ? language.trim() : '',
};
};
/**
* Expose `matter`
*/
matter.cache = {};
matter.clearCache = function () {
matter.cache = {};
};
export default matter;

View file

@ -1,18 +0,0 @@
'use strict';
import engines from './engines.js';
import * as utils from './utils.js';
export default function (options) {
const opts = Object.assign({}, options);
// ensure that delimiters are an array
opts.delimiters = utils.arrayify(opts.delims || opts.delimiters || '---');
if (opts.delimiters.length === 1) {
opts.delimiters.push(opts.delimiters[0]);
}
opts.language = (opts.language || opts.lang || 'yaml').toLowerCase();
opts.engines = Object.assign({}, engines, opts.parsers, opts.engines);
return opts;
}

View file

@ -1,30 +0,0 @@
'use strict';
export default function (name, options) {
let engine = options.engines[name] || options.engines[aliase(name)];
if (typeof engine === 'undefined') {
throw new Error('gray-matter engine "' + name + '" is not registered');
}
if (typeof engine === 'function') {
engine = { parse: engine };
}
return engine;
}
function aliase(name) {
switch (name.toLowerCase()) {
case 'js':
case 'javascript':
return 'javascript';
case 'coffee':
case 'coffeescript':
case 'cson':
return 'coffee';
case 'yaml':
case 'yml':
return 'yaml';
default: {
return name;
}
}
}

View file

@ -1,56 +0,0 @@
'use strict';
import yaml from 'js-yaml';
/**
* Default engines
*/
const engines = {};
/**
* YAML
*/
engines.yaml = {
parse: yaml.safeLoad.bind(yaml),
stringify: yaml.safeDump.bind(yaml),
};
/**
* JSON
*/
engines.json = {
parse: JSON.parse.bind(JSON),
stringify: function (obj, options) {
const opts = Object.assign({ replacer: null, space: 2 }, options);
return JSON.stringify(obj, opts.replacer, opts.space);
},
};
/**
* JavaScript
*/
engines.javascript = {
parse: function parse(str, options, wrap) {
/* eslint no-eval: 0 */
try {
if (wrap !== false) {
str = '(function() {\nreturn ' + str.trim() + ';\n}());';
}
return eval(str) || {};
} catch (err) {
if (wrap !== false && /(unexpected|identifier)/i.test(err.message)) {
return parse(str, options, false);
}
throw new SyntaxError(err);
}
},
stringify: function () {
throw new Error('stringifying JavaScript is not supported');
},
};
export default engines;

View file

@ -1,30 +0,0 @@
'use strict';
import defaults from './defaults.js';
export default function (file, options) {
const opts = defaults(options);
if (file.data == null) {
file.data = {};
}
if (typeof opts.excerpt === 'function') {
return opts.excerpt(file, opts);
}
const sep = file.data.excerpt_separator || opts.excerpt_separator;
if (sep == null && (opts.excerpt === false || opts.excerpt == null)) {
return file;
}
const delimiter = typeof opts.excerpt === 'string' ? opts.excerpt : sep || opts.delimiters[0];
// if enabled, get the excerpt defined after front-matter
const idx = file.content.indexOf(delimiter);
if (idx !== -1) {
file.excerpt = file.content.slice(0, idx);
}
return file;
}

View file

@ -1,13 +0,0 @@
'use strict';
import getEngine from './engine.js';
import defaults from './defaults.js';
export default function (language, str, options) {
const opts = defaults(options);
const engine = getEngine(language, opts);
if (typeof engine.parse !== 'function') {
throw new TypeError('expected "' + language + '.parse" to be a function');
}
return engine.parse(str, opts);
}

View file

@ -1,56 +0,0 @@
'use strict';
import typeOf from 'kind-of';
import getEngine from './engine.js';
import defaults from './defaults.js';
export default function (file, data, options) {
if (data == null && options == null) {
switch (typeOf(file)) {
case 'object':
data = file.data;
options = {};
break;
case 'string':
return file;
default: {
throw new TypeError('expected file to be a string or object');
}
}
}
const str = file.content;
const opts = defaults(options);
if (data == null) {
if (!opts.data) return file;
data = opts.data;
}
const language = file.language || opts.language;
const engine = getEngine(language, opts);
if (typeof engine.stringify !== 'function') {
throw new TypeError('expected "' + language + '.stringify" to be a function');
}
data = Object.assign({}, file.data, data);
const open = opts.delimiters[0];
const close = opts.delimiters[1];
const matter = engine.stringify(data, options).trim();
let buf = '';
if (matter !== '{}') {
buf = newline(open) + newline(matter) + newline(close);
}
if (typeof file.excerpt === 'string' && file.excerpt !== '') {
if (str.indexOf(file.excerpt.trim()) === -1) {
buf += newline(file.excerpt) + newline(close);
}
}
return buf + newline(str);
}
function newline(str) {
return str.slice(-1) !== '\n' ? str + '\n' : str;
}

View file

@ -1,43 +0,0 @@
'use strict';
import typeOf from 'kind-of';
import stringify from './stringify.js';
import * as utils from './utils.js';
/**
* Normalize the given value to ensure an object is returned
* with the expected properties.
*/
export default function (file) {
if (typeOf(file) !== 'object') {
file = { content: file };
}
if (typeOf(file.data) !== 'object') {
file.data = {};
}
// if file was passed as an object, ensure that
// "file.content" is set
if (file.contents && file.content == null) {
file.content = file.contents;
}
// set non-enumerable properties on the file object
utils.define(file, 'orig', utils.toBuffer(file.content));
utils.define(file, 'language', file.language || '');
utils.define(file, 'matter', file.matter || '');
utils.define(file, 'stringify', function (data, options) {
if (options && options.language) {
file.language = options.language;
}
return stringify(file, data, options);
});
// strip BOM and ensure that "file.content" is a string
file.content = utils.toString(file.content);
file.isEmpty = false;
file.excerpt = '';
return file;
}

View file

@ -1,66 +0,0 @@
'use strict';
import stripBom from 'strip-bom-string';
import typeOf from 'kind-of';
export function define(obj, key, val) {
Reflect.defineProperty(obj, key, {
enumerable: false,
configurable: true,
writable: true,
value: val,
});
}
/**
* Returns true if `val` is a buffer
*/
export function isBuffer(val) {
return typeOf(val) === 'buffer';
}
/**
* Returns true if `val` is an object
*/
export function isObject(val) {
return typeOf(val) === 'object';
}
/**
* Cast `input` to a buffer
*/
export function toBuffer(input) {
return typeof input === 'string' ? Buffer.from(input) : input;
}
/**
* Cast `val` to a string.
*/
export function toString(input) {
if (isBuffer(input)) return stripBom(String(input));
if (typeof input !== 'string') {
throw new TypeError('expected input to be a string or buffer');
}
return stripBom(input);
}
/**
* Cast `val` to an array.
*/
export function arrayify(val) {
return val ? (Array.isArray(val) ? val : [val]) : [];
}
/**
* Returns true if `str` starts with `substr`.
*/
export function startsWith(str, substr, len) {
if (typeof len !== 'number') len = substr.length;
return str.slice(0, len) === substr;
}

View file

@ -1,12 +1,12 @@
{
"name": "@astrojs/markdown-support",
"name": "@astrojs/markdown-remark",
"version": "0.3.1",
"main": "./dist/index.js",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/snowpackjs/astro.git",
"directory": "packages/markdown-support"
"directory": "packages/markdown/remark"
},
"exports": {
".": "./dist/index.js"
@ -23,10 +23,13 @@
"github-slugger": "^1.3.0",
"mdast-util-mdx-expression": "^1.1.0",
"micromark-extension-mdx-expression": "^1.0.0",
"micromark-extension-mdx-jsx": "^1.0.0",
"mdast-util-mdx-jsx": "^1.1.0",
"rehype-raw": "^6.0.0",
"rehype-stringify": "^9.0.1",
"remark-footnotes": "^4.0.1",
"remark-gfm": "^2.0.0",
"remark-mdx": "^1.6.22",
"remark-parse": "^10.0.0",
"remark-rehype": "^9.0.0",
"remark-slug": "^7.0.0",
@ -34,7 +37,9 @@
"unist-util-map": "^3.0.0",
"unist-util-visit": "^4.0.0"
},
"//": "Important that gray-matter is in devDependencies so it gets bundled by esbuild!",
"devDependencies": {
"@types/github-slugger": "^1.3.0"
"@types/github-slugger": "^1.3.0",
"gray-matter": "^4.0.3"
}
}

View file

@ -23,7 +23,7 @@ export function rehypeCodeBlock() {
const escapeCode = (code: Element): void => {
code.children = code.children.map((child) => {
if (child.type === 'text') {
return { ...child, value: child.value.replace(/\{/g, 'ASTRO_ESCAPED_LEFT_CURLY_BRACKET\0') };
return { ...child, value: child.value.replace(/\{/g, '&lbrace;') };
}
return child;
});

View file

@ -4,16 +4,17 @@ import createCollectHeaders from './rehype-collect-headers.js';
import scopedStyles from './remark-scoped-styles.js';
import { remarkExpressions, loadRemarkExpressions } from './remark-expressions.js';
import rehypeExpressions from './rehype-expressions.js';
import { remarkJsx, loadRemarkJsx } from './remark-jsx.js';
import rehypeJsx from './rehype-jsx.js';
import { remarkCodeBlock, rehypeCodeBlock } from './codeblock.js';
import remarkSlug from './remark-slug.js';
import { loadPlugins } from './load-plugins.js';
import raw from 'rehype-raw';
import { unified } from 'unified';
import markdown from 'remark-parse';
import markdownToHtml from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import remarkSlug from 'remark-slug';
import matter from './gray-matter/index.js';
import matter from 'gray-matter';
export { AstroMarkdownOptions, MarkdownRenderingOptions };
@ -24,29 +25,29 @@ export async function renderMarkdownWithFrontmatter(contents: string, opts?: Mar
return { ...value, frontmatter };
}
export const DEFAULT_REMARK_PLUGINS = [
'remark-gfm',
'remark-footnotes',
// TODO: reenable smartypants!
'@silvenon/remark-smartypants'
]
export const DEFAULT_REHYPE_PLUGINS = [
// empty
]
/** Shared utility for rendering markdown */
export async function renderMarkdown(content: string, opts?: MarkdownRenderingOptions | null) {
const { $: { scopedClassName = null } = {}, footnotes: useFootnotes = true, gfm: useGfm = true, remarkPlugins = [], rehypePlugins = [] } = opts ?? {};
const { remarkPlugins = DEFAULT_REMARK_PLUGINS, rehypePlugins = DEFAULT_REHYPE_PLUGINS } = opts ?? {};
const { headers, rehypeCollectHeaders } = createCollectHeaders();
await loadRemarkExpressions(); // Vite bug: dynamically import() these because of CJS interop (this will cache)
await Promise.all([loadRemarkExpressions(), loadRemarkJsx()]); // Vite bug: dynamically import() these because of CJS interop (this will cache)
let parser = unified()
.use(markdown)
.use(remarkSlug)
.use([remarkExpressions, { addResult: true }]);
.use([remarkJsx])
.use([remarkExpressions])
if (remarkPlugins.length === 0) {
if (useGfm) {
remarkPlugins.push('remark-gfm');
}
if (useFootnotes) {
remarkPlugins.push('remark-footnotes');
}
remarkPlugins.push('@silvenon/remark-smartypants');
}
const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins));
@ -54,25 +55,25 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
parser.use(plugin, opts);
});
if (scopedClassName) {
parser.use(scopedStyles(scopedClassName));
}
// if (scopedClassName) {
// parser.use(scopedStyles(scopedClassName));
// }
parser.use(remarkCodeBlock);
parser.use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw', 'mdxTextExpression'] });
parser.use(rehypeExpressions);
parser.use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw', 'mdxTextExpression', 'mdxJsxTextElement', 'mdxJsxFlowElement']});
loadedRehypePlugins.forEach(([plugin, opts]) => {
parser.use(plugin, opts);
});
parser.use(rehypeJsx).use(rehypeExpressions)
let result: string;
try {
const vfile = await parser
.use(raw)
.use(rehypeCollectHeaders)
.use(rehypeCodeBlock)
.use(rehypeStringify, { entities: { useNamedReferences: true } })
.use(rehypeStringify, { allowParseErrors: true, preferUnquoted: true, allowDangerousHtml: true })
.process(content);
result = vfile.toString();
} catch (err) {
@ -80,7 +81,9 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
}
return {
astro: { headers, source: content, html: result.toString() },
content: result.toString(),
metadata: { headers, source: content, html: result.toString() },
code: result.toString(),
};
}
export default renderMarkdownWithFrontmatter;

View file

@ -0,0 +1,27 @@
import { map } from 'unist-util-map';
const MDX_ELEMENTS = new Set(['mdxJsxFlowElement', 'mdxJsxTextElement']);
export default function rehypeJsx(): any {
return function (node: any): any {
return map(node, (child) => {
if (child.type === 'element') {
return { ...child, tagName: `${child.tagName}` }
}
if (MDX_ELEMENTS.has(child.type)) {
return {
...child,
type: 'element',
tagName: `${child.name}`,
properties: child.attributes.reduce((acc, entry) => {
let attr = entry.value;
if (attr && typeof attr === 'object') {
attr = `{${attr.value}}`
}
return Object.assign(acc, { [entry.name]: attr });
}, {})
};
}
return child;
});
};
}

View file

@ -0,0 +1,31 @@
// Vite bug: dynamically import() modules needed for CJS. Cache in memory to keep side effects
let mdxJsx: any;
let mdxJsxFromMarkdown: any;
let mdxJsxToMarkdown: any;
export function remarkJsx(this: any, options: any) {
let settings = options || {};
let data = this.data();
add('micromarkExtensions', mdxJsx({}));
add('fromMarkdownExtensions', mdxJsxFromMarkdown);
add('toMarkdownExtensions', mdxJsxToMarkdown);
function add(field: any, value: any) {
/* istanbul ignore if - other extensions. */
if (data[field]) data[field].push(value);
else data[field] = [value];
}
}
export async function loadRemarkJsx() {
if (!mdxJsx) {
const micromarkMdxJsx = await import('micromark-extension-mdx-jsx');
mdxJsx = micromarkMdxJsx.mdxJsx;
}
if (!mdxJsxFromMarkdown || !mdxJsxToMarkdown) {
const mdastUtilMdxJsx = await import('mdast-util-mdx-jsx');
mdxJsxFromMarkdown = mdastUtilMdxJsx.mdxJsxFromMarkdown;
mdxJsxToMarkdown = mdastUtilMdxJsx.mdxJsxToMarkdown;
}
}

View file

@ -0,0 +1,34 @@
/**
* @typedef {import('mdast').Root} Root
* @typedef {import('hast').Properties} Properties
*/
import {toString} from 'mdast-util-to-string'
import {visit} from 'unist-util-visit'
import BananaSlug from 'github-slugger'
const slugs = new BananaSlug()
/**
* Plugin to add anchors headings using GitHubs algorithm.
*
* @type {import('unified').Plugin<void[], Root>}
*/
export default function remarkSlug() {
return (tree: any) => {
slugs.reset()
visit(tree, (node) => {
console.log(node);
});
visit(tree, 'heading', (node) => {
const data = node.data || (node.data = {})
const props = /** @type {Properties} */ (
data.hProperties || (data.hProperties = {})
)
let id = props.id
id = id ? slugs.slug(String(id), true) : slugs.slug(toString(node))
data.id = id;
props.id = id;
})
}
}

View file

@ -4,12 +4,8 @@ export type UnifiedPluginImport = Promise<{ default: unified.Plugin }>;
export type Plugin = string | [string, any] | UnifiedPluginImport | [UnifiedPluginImport, any];
export interface AstroMarkdownOptions {
/** Enable or disable footnotes syntax extension */
footnotes: boolean;
/** Enable or disable GitHub-flavored Markdown syntax extension */
gfm: boolean;
remarkPlugins: Plugin[];
rehypePlugins: Plugin[];
remarkPlugins?: Plugin[];
rehypePlugins?: Plugin[];
}
export interface MarkdownRenderingOptions extends Partial<AstroMarkdownOptions> {

683
yarn.lock

File diff suppressed because it is too large Load diff