Add Markdown support
This commit is contained in:
parent
3df506a219
commit
5909f27b19
43 changed files with 1035 additions and 808 deletions
9
examples/blog/src/components/Heading.astro
Normal file
9
examples/blog/src/components/Heading.astro
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<h1>
|
||||||
|
<slot/>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,12 +1,12 @@
|
||||||
---
|
---
|
||||||
import { Markdown } from 'astro/components';
|
|
||||||
import BaseHead from '../components/BaseHead.astro';
|
import BaseHead from '../components/BaseHead.astro';
|
||||||
import BlogHeader from '../components/BlogHeader.astro';
|
// import BlogHeader from '../components/BlogHeader.astro';
|
||||||
import BlogPost from '../components/BlogPost.astro';
|
// import BlogPost from '../components/BlogPost.astro';
|
||||||
|
|
||||||
const {content} = Astro.props;
|
const {content} = Astro.props;
|
||||||
const {title, description, publishDate, author, heroImage, permalink, alt} = content;
|
const {title, description, publishDate, author, heroImage, permalink, alt} = content;
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang={ content.lang || 'en' }>
|
<html lang={ content.lang || 'en' }>
|
||||||
<head>
|
<head>
|
||||||
<BaseHead title={title} description={description} permalink={permalink} />
|
<BaseHead title={title} description={description} permalink={permalink} />
|
||||||
|
@ -14,10 +14,13 @@ const {title, description, publishDate, author, heroImage, permalink, alt} = con
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<BlogHeader />
|
<h1>Hello world!</h1>
|
||||||
<BlogPost title={title} author={author} heroImage={heroImage} publishDate={publishDate} alt={alt}>
|
<div class="container">
|
||||||
<slot />
|
<slot />
|
||||||
</BlogPost>
|
</div>
|
||||||
|
<!-- <BlogHeader /> -->
|
||||||
|
<!-- <BlogPost title={title} author={author} heroImage={heroImage} publishDate={publishDate} alt={alt}> -->
|
||||||
|
<!-- </BlogPost> -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ let permalink = 'https://example.com/';
|
||||||
|
|
||||||
// Data Fetching: List all Markdown posts in the repo.
|
// 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());
|
allPosts = allPosts.sort((a, b) => new Date(b.publishDate).valueOf() - new Date(a.publishDate).valueOf());
|
||||||
|
|
||||||
// Full Astro Component Syntax:
|
// Full Astro Component Syntax:
|
||||||
|
|
15
examples/blog/src/pages/posts/index.md
Normal file
15
examples/blog/src/pages/posts/index.md
Normal 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}?
|
|
@ -12,11 +12,11 @@
|
||||||
"build": "yarn build:core",
|
"build": "yarn build:core",
|
||||||
"build:one": "lerna run build --scope",
|
"build:one": "lerna run build --scope",
|
||||||
"build:all": "lerna run build --scope \"{astro,@astrojs/*}\"",
|
"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": "yarn dev:core --parallel --stream",
|
||||||
"dev:one": "lerna run dev --scope --parallel --stream",
|
"dev:one": "lerna run dev --scope --parallel --stream",
|
||||||
"dev:all": "lerna run dev --scope \"{astro,@astrojs/*}\" --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 .",
|
"format": "prettier -w .",
|
||||||
"lint": "eslint \"packages/**/*.ts\"",
|
"lint": "eslint \"packages/**/*.ts\"",
|
||||||
"test": "yarn workspace astro run test",
|
"test": "yarn workspace astro run test",
|
||||||
|
@ -24,6 +24,7 @@
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"compiled/*",
|
"compiled/*",
|
||||||
|
"packages/markdown/*",
|
||||||
"packages/renderers/*",
|
"packages/renderers/*",
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"examples/*",
|
"examples/*",
|
||||||
|
@ -45,6 +46,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.16.0",
|
"@changesets/cli": "^2.16.0",
|
||||||
"@types/jest": "^27.0.1",
|
"@types/jest": "^27.0.1",
|
||||||
|
"@octokit/action": "^3.15.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||||
"@typescript-eslint/parser": "^4.18.0",
|
"@typescript-eslint/parser": "^4.18.0",
|
||||||
"autoprefixer": "^10.2.6",
|
"autoprefixer": "^10.2.6",
|
||||||
|
|
|
@ -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 {
|
export interface Props {
|
||||||
content?: string;
|
content?: string;
|
||||||
|
|
|
@ -39,8 +39,8 @@
|
||||||
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^0.1.0-canary.36",
|
"@astrojs/compiler": "^0.1.0-canary.37",
|
||||||
"@astrojs/markdown-support": "^0.3.1",
|
"@astrojs/markdown-remark": "^0.3.1",
|
||||||
"@babel/core": "^7.15.0",
|
"@babel/core": "^7.15.0",
|
||||||
"@web/rollup-plugin-html": "^1.9.1",
|
"@web/rollup-plugin-html": "^1.9.1",
|
||||||
"astring": "^1.7.5",
|
"astring": "^1.7.5",
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import type { AstroMarkdownOptions } from '@astrojs/markdown-support';
|
|
||||||
import type babel from '@babel/core';
|
import type babel from '@babel/core';
|
||||||
import type vite from 'vite';
|
import type vite from 'vite';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
import type { AstroConfigSchema } from '../config';
|
import type { AstroConfigSchema } from '../config';
|
||||||
import type { AstroComponentFactory } from '../internal';
|
import type { AstroComponentFactory } from '../internal';
|
||||||
|
|
||||||
export { AstroMarkdownOptions };
|
|
||||||
|
|
||||||
export interface AstroComponentMetadata {
|
export interface AstroComponentMetadata {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
hydrate?: 'load' | 'idle' | 'visible' | 'media' | 'only';
|
hydrate?: 'load' | 'idle' | 'visible' | 'media' | 'only';
|
||||||
|
@ -59,7 +56,9 @@ export interface AstroUserConfig {
|
||||||
*/
|
*/
|
||||||
renderers?: string[];
|
renderers?: string[];
|
||||||
/** Options for rendering markdown content */
|
/** Options for rendering markdown content */
|
||||||
markdownOptions?: Partial<AstroMarkdownOptions>;
|
markdownOptions?: {
|
||||||
|
render?: [string, Record<string, any>];
|
||||||
|
};
|
||||||
/** Options specific to `astro build` */
|
/** Options specific to `astro build` */
|
||||||
buildOptions?: {
|
buildOptions?: {
|
||||||
/** Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs. */
|
/** 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> {
|
// export interface AstroUserConfig extends z.input<typeof AstroConfigSchema> {
|
||||||
// markdownOptions?: Partial<AstroMarkdownOptions>;
|
// markdownOptions?: Partial<AstroMarkdownOptions>;
|
||||||
// }
|
// }
|
||||||
export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
|
export type AstroConfig = z.output<typeof AstroConfigSchema>;
|
||||||
markdownOptions: Partial<AstroMarkdownOptions>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AsyncRendererComponentFn<U> = (Component: any, props: any, children: string | undefined, metadata?: AstroComponentMetadata) => Promise<U>;
|
export type AsyncRendererComponentFn<U> = (Component: any, props: any, children: string | undefined, metadata?: AstroComponentMetadata) => Promise<U>;
|
||||||
|
|
||||||
|
|
|
@ -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>;
|
|
||||||
}
|
|
|
@ -1 +1 @@
|
||||||
export { AstroConfig, AstroUserConfig } from './config';
|
export { AstroConfig, AstroUserConfig } from './astro';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { AstroConfig } from './@types/astro';
|
import type { AstroConfig, AstroUserConfig } from './@types/astro';
|
||||||
|
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import getPort from 'get-port';
|
import getPort from 'get-port';
|
||||||
|
@ -6,7 +6,6 @@ import * as colors from 'kleur/colors';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { pathToFileURL } from 'url';
|
import { pathToFileURL } from 'url';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { AstroUserConfig } from './@types/config';
|
|
||||||
|
|
||||||
export const AstroConfigSchema = z.object({
|
export const AstroConfigSchema = z.object({
|
||||||
projectRoot: z
|
projectRoot: z
|
||||||
|
@ -41,6 +40,7 @@ export const AstroConfigSchema = z.object({
|
||||||
gfm: z.boolean().optional(),
|
gfm: z.boolean().optional(),
|
||||||
remarkPlugins: z.array(z.any()).optional(),
|
remarkPlugins: z.array(z.any()).optional(),
|
||||||
rehypePlugins: z.array(z.any()).optional(),
|
rehypePlugins: z.array(z.any()).optional(),
|
||||||
|
render: z.any().optional().default(['@astrojs/markdown-remark', {}]),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({}),
|
.default({}),
|
||||||
|
|
|
@ -12,6 +12,8 @@ import { performance } from 'perf_hooks';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { createRequire } from 'module';
|
import { createRequire } from 'module';
|
||||||
import stripAnsi from 'strip-ansi';
|
import stripAnsi from 'strip-ansi';
|
||||||
|
import path from 'path';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
import vite from 'vite';
|
import vite from 'vite';
|
||||||
import { defaultLogOptions, error, info } from '../logger.js';
|
import { defaultLogOptions, error, info } from '../logger.js';
|
||||||
import { createRouteManifest, matchRoute } from '../runtime/routing.js';
|
import { createRouteManifest, matchRoute } from '../runtime/routing.js';
|
||||||
|
|
|
@ -3,9 +3,10 @@ import type { AstroComponentMetadata } from '../@types/astro';
|
||||||
import { valueToEstree, Value } from 'estree-util-value-to-estree';
|
import { valueToEstree, Value } from 'estree-util-value-to-estree';
|
||||||
import * as astring from 'astring';
|
import * as astring from 'astring';
|
||||||
import shorthash from 'shorthash';
|
import shorthash from 'shorthash';
|
||||||
import { renderAstroComponent } from '../runtime/astro.js';
|
import { renderToString, renderAstroComponent } from '../runtime/astro.js';
|
||||||
|
|
||||||
const { generate, GENERATOR } = astring;
|
const { generate, GENERATOR } = astring;
|
||||||
|
|
||||||
// A more robust version alternative to `JSON.stringify` that can handle most values
|
// A more robust version alternative to `JSON.stringify` that can handle most values
|
||||||
// see https://github.com/remcohaszing/estree-util-value-to-estree#readme
|
// see https://github.com/remcohaszing/estree-util-value-to-estree#readme
|
||||||
const customGenerator: astring.Generator = {
|
const customGenerator: astring.Generator = {
|
||||||
|
@ -25,16 +26,21 @@ const serialize = (value: Value) =>
|
||||||
generator: customGenerator,
|
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.
|
// Special: If a child is a function, call it automatically.
|
||||||
// This lets you do {() => ...} without the extra boilerplate
|
// This lets you do {() => ...} without the extra boilerplate
|
||||||
// of wrapping it in a function and calling it.
|
// of wrapping it in a function and calling it.
|
||||||
if (typeof child === 'function') {
|
|
||||||
return await child();
|
return await child();
|
||||||
} else if (typeof child === 'string') {
|
} else if (typeof child === 'string') {
|
||||||
return child;
|
return child;
|
||||||
} else if (!child && child !== 0) {
|
} else if (!child && child !== 0) {
|
||||||
// do nothing, safe to ignore falsey values.
|
// do nothing, safe to ignore falsey values.
|
||||||
|
} else if (child instanceof AstroComponent) {
|
||||||
|
return await renderAstroComponent(child);
|
||||||
} else {
|
} else {
|
||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
|
@ -43,7 +49,6 @@ async function _render(child: any) {
|
||||||
export class AstroComponent {
|
export class AstroComponent {
|
||||||
private htmlParts: string[];
|
private htmlParts: string[];
|
||||||
private expressions: TemplateStringsArray;
|
private expressions: TemplateStringsArray;
|
||||||
|
|
||||||
constructor(htmlParts: string[], expressions: TemplateStringsArray) {
|
constructor(htmlParts: string[], expressions: TemplateStringsArray) {
|
||||||
this.htmlParts = htmlParts;
|
this.htmlParts = htmlParts;
|
||||||
this.expressions = expressions;
|
this.expressions = expressions;
|
||||||
|
@ -129,12 +134,20 @@ setup("${astroId}", {${metadata.hydrateArgs ? `value: ${JSON.stringify(metadata.
|
||||||
return hydrationScript;
|
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;
|
Component = await Component;
|
||||||
// children = await renderGenerator(children);
|
// children = await renderGenerator(children);
|
||||||
const { renderers } = result._metadata;
|
const { renderers } = result._metadata;
|
||||||
|
|
||||||
if (Component && (Component as any).isAstroComponentFactory) {
|
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;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import '@vite/client';
|
import '/@vite/client';
|
||||||
|
|
||||||
if (import.meta.hot) {
|
if (import.meta.hot) {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -2,6 +2,7 @@ import type { BuildResult } from 'esbuild';
|
||||||
import type { ViteDevServer } from 'vite';
|
import type { ViteDevServer } from 'vite';
|
||||||
import type { AstroConfig, ComponentInstance, GetStaticPathsResult, Params, Props, RouteCache, RouteData, RuntimeMode, SSRError } from '../@types/astro';
|
import type { AstroConfig, ComponentInstance, GetStaticPathsResult, Params, Props, RouteCache, RouteData, RuntimeMode, SSRError } from '../@types/astro';
|
||||||
import type { LogOptions } from '../logger';
|
import type { LogOptions } from '../logger';
|
||||||
|
import type { PathsOutput } from 'fdir';
|
||||||
|
|
||||||
import cheerio from 'cheerio';
|
import cheerio from 'cheerio';
|
||||||
import * as eslexer from 'es-module-lexer';
|
import * as eslexer from 'es-module-lexer';
|
||||||
|
@ -9,6 +10,7 @@ import { fileURLToPath } from 'url';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { renderPage } from './astro.js';
|
import { renderPage } from './astro.js';
|
||||||
|
import { fdir } from 'fdir';
|
||||||
import { generatePaginateFunction } from './paginate.js';
|
import { generatePaginateFunction } from './paginate.js';
|
||||||
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
|
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
|
||||||
import { parseNpmName, canonicalURL as getCanonicalURL, codeFrame } from './util.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> = {};
|
let importedModules: Record<string, any> = {};
|
||||||
const moduleNodes = Array.from(modulesByFile);
|
const moduleNodes = Array.from(modulesByFile);
|
||||||
|
|
||||||
// Loop over the importedModules and grab the exports from each one.
|
// Loop over the importedModules and grab the exports from each one.
|
||||||
// We'll pass these to the shared $$result so renderers can match
|
// We'll pass these to the shared $$result so renderers can match
|
||||||
// components to their exported identifier and URL
|
// 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 fullURL = new URL(pathname, origin);
|
||||||
|
|
||||||
const Component = await mod.default;
|
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})`);
|
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
|
||||||
|
|
||||||
let html = await renderPage(
|
const result = {
|
||||||
{
|
|
||||||
styles: new Set(),
|
styles: new Set(),
|
||||||
scripts: new Set(),
|
scripts: new Set(),
|
||||||
/** This function returns the `Astro` faux-global */
|
/** This function returns the `Astro` faux-global */
|
||||||
createAstro(props: any) {
|
createAstro: (props: any) => {
|
||||||
const site = new URL(origin);
|
const site = new URL(origin);
|
||||||
const url = new URL('.' + pathname, site);
|
const url = new URL('.' + pathname, site);
|
||||||
const canonicalURL = getCanonicalURL(pathname, astroConfig.buildOptions.site || origin);
|
const canonicalURL = getCanonicalURL(pathname, astroConfig.buildOptions.site || origin)
|
||||||
return { isPage: true, site, request: { url, canonicalURL }, props };
|
const fetchContent = createFetchContent(fileURLToPath(filePath));
|
||||||
|
return {
|
||||||
|
isPage: true,
|
||||||
|
site,
|
||||||
|
request: { url, canonicalURL },
|
||||||
|
props,
|
||||||
|
fetchContent
|
||||||
|
};
|
||||||
},
|
},
|
||||||
_metadata: { importedModules, renderers },
|
_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,
|
}, ChildComponent, {}, null);
|
||||||
{},
|
}))
|
||||||
null
|
return contents;
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = await renderPage(result, Component, {}, null);
|
||||||
|
|
||||||
// 4. modify response
|
// 4. modify response
|
||||||
if (mode === 'development') {
|
if (mode === 'development') {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { fileURLToPath } from 'url';
|
||||||
import { createRequire } from 'module';
|
import { createRequire } from 'module';
|
||||||
import { getPackageJSON, parseNpmName } from '../util.js';
|
import { getPackageJSON, parseNpmName } from '../util.js';
|
||||||
import astro from './plugin-astro.js';
|
import astro from './plugin-astro.js';
|
||||||
|
import markdown from './plugin-markdown.js';
|
||||||
import jsx from './plugin-jsx.js';
|
import jsx from './plugin-jsx.js';
|
||||||
import { AstroDevServer } from '../../dev';
|
import { AstroDevServer } from '../../dev';
|
||||||
|
|
||||||
|
@ -34,6 +35,7 @@ export async function loadViteConfig(
|
||||||
});
|
});
|
||||||
const userDevDeps = Object.keys(packageJSON?.devDependencies || {});
|
const userDevDeps = Object.keys(packageJSON?.devDependencies || {});
|
||||||
const { external, noExternal } = await viteSSRDeps([...userDeps, ...userDevDeps]);
|
const { external, noExternal } = await viteSSRDeps([...userDeps, ...userDevDeps]);
|
||||||
|
// console.log(external.has('tiny-glob'), noExternal.has('tiny-glob'));
|
||||||
|
|
||||||
// load Astro renderers
|
// load Astro renderers
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
@ -78,7 +80,7 @@ export async function loadViteConfig(
|
||||||
/** Always include these dependencies for optimization */
|
/** Always include these dependencies for optimization */
|
||||||
include: [...optimizedDeps],
|
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),
|
publicDir: fileURLToPath(astroConfig.public),
|
||||||
resolve: {
|
resolve: {
|
||||||
dedupe: [...dedupe],
|
dedupe: [...dedupe],
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
import type { TransformResult } from '@astrojs/compiler';
|
||||||
import type { Plugin } from 'vite';
|
import type { Plugin } from 'vite';
|
||||||
import type { AstroConfig, Renderer } from '../../@types/astro.js';
|
import type { AstroConfig, Renderer } from '../../@types/astro.js';
|
||||||
import type { LogOptions } from '../../logger';
|
|
||||||
|
|
||||||
import esbuild from 'esbuild';
|
import esbuild from 'esbuild';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { transform } from '@astrojs/compiler';
|
import { transform } from '@astrojs/compiler';
|
||||||
|
import { decode } from 'sourcemap-codec';
|
||||||
import { AstroDevServer } from '../../dev/index.js';
|
import { AstroDevServer } from '../../dev/index.js';
|
||||||
|
|
||||||
interface AstroPluginOptions {
|
interface AstroPluginOptions {
|
||||||
|
@ -13,18 +14,21 @@ interface AstroPluginOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Transform .astro files for Vite */
|
/** Transform .astro files for Vite */
|
||||||
export default function astro({ devServer }: AstroPluginOptions): Plugin {
|
export default function astro({ config, devServer }: AstroPluginOptions): Plugin {
|
||||||
return {
|
return {
|
||||||
name: '@astrojs/vite-plugin-astro',
|
name: '@astrojs/vite-plugin-astro',
|
||||||
enforce: 'pre', // run transforms before other plugins can
|
enforce: 'pre', // run transforms before other plugins can
|
||||||
// note: don’t claim .astro files with resolveId() — it prevents Vite from transpiling the final JS (import.meta.globEager, etc.)
|
// note: don’t claim .astro files with resolveId() — it prevents Vite from transpiling the final JS (import.meta.globEager, etc.)
|
||||||
async load(id) {
|
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 source = await fs.promises.readFile(id, 'utf8');
|
||||||
|
let tsResult: TransformResult | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
// 1. Transform from `.astro` to valid `.ts`
|
// 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.
|
// 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`
|
// 2. Compile `.ts` to `.js`
|
||||||
const { code, map } = await esbuild.transform(tsResult.code, { loader: 'ts', sourcemap: 'inline', sourcefile: id });
|
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,
|
code,
|
||||||
map,
|
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;
|
return null;
|
||||||
},
|
},
|
||||||
async handleHotUpdate(context) {
|
async handleHotUpdate(context) {
|
||||||
|
|
68
packages/astro/src/runtime/vite/plugin-markdown.ts
Normal file
68
packages/astro/src/runtime/vite/plugin-markdown.ts
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@astrojs/markdown-support",
|
"name": "@astrojs/markdown-remark",
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/snowpackjs/astro.git",
|
"url": "https://github.com/snowpackjs/astro.git",
|
||||||
"directory": "packages/markdown-support"
|
"directory": "packages/markdown/remark"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js"
|
".": "./dist/index.js"
|
||||||
|
@ -23,10 +23,13 @@
|
||||||
"github-slugger": "^1.3.0",
|
"github-slugger": "^1.3.0",
|
||||||
"mdast-util-mdx-expression": "^1.1.0",
|
"mdast-util-mdx-expression": "^1.1.0",
|
||||||
"micromark-extension-mdx-expression": "^1.0.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-raw": "^6.0.0",
|
||||||
"rehype-stringify": "^9.0.1",
|
"rehype-stringify": "^9.0.1",
|
||||||
"remark-footnotes": "^4.0.1",
|
"remark-footnotes": "^4.0.1",
|
||||||
"remark-gfm": "^2.0.0",
|
"remark-gfm": "^2.0.0",
|
||||||
|
"remark-mdx": "^1.6.22",
|
||||||
"remark-parse": "^10.0.0",
|
"remark-parse": "^10.0.0",
|
||||||
"remark-rehype": "^9.0.0",
|
"remark-rehype": "^9.0.0",
|
||||||
"remark-slug": "^7.0.0",
|
"remark-slug": "^7.0.0",
|
||||||
|
@ -34,7 +37,9 @@
|
||||||
"unist-util-map": "^3.0.0",
|
"unist-util-map": "^3.0.0",
|
||||||
"unist-util-visit": "^4.0.0"
|
"unist-util-visit": "^4.0.0"
|
||||||
},
|
},
|
||||||
|
"//": "Important that gray-matter is in devDependencies so it gets bundled by esbuild!",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/github-slugger": "^1.3.0"
|
"@types/github-slugger": "^1.3.0",
|
||||||
|
"gray-matter": "^4.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -23,7 +23,7 @@ export function rehypeCodeBlock() {
|
||||||
const escapeCode = (code: Element): void => {
|
const escapeCode = (code: Element): void => {
|
||||||
code.children = code.children.map((child) => {
|
code.children = code.children.map((child) => {
|
||||||
if (child.type === 'text') {
|
if (child.type === 'text') {
|
||||||
return { ...child, value: child.value.replace(/\{/g, 'ASTRO_ESCAPED_LEFT_CURLY_BRACKET\0') };
|
return { ...child, value: child.value.replace(/\{/g, '{') };
|
||||||
}
|
}
|
||||||
return child;
|
return child;
|
||||||
});
|
});
|
|
@ -4,16 +4,17 @@ import createCollectHeaders from './rehype-collect-headers.js';
|
||||||
import scopedStyles from './remark-scoped-styles.js';
|
import scopedStyles from './remark-scoped-styles.js';
|
||||||
import { remarkExpressions, loadRemarkExpressions } from './remark-expressions.js';
|
import { remarkExpressions, loadRemarkExpressions } from './remark-expressions.js';
|
||||||
import rehypeExpressions from './rehype-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 { remarkCodeBlock, rehypeCodeBlock } from './codeblock.js';
|
||||||
|
import remarkSlug from './remark-slug.js';
|
||||||
import { loadPlugins } from './load-plugins.js';
|
import { loadPlugins } from './load-plugins.js';
|
||||||
import raw from 'rehype-raw';
|
|
||||||
|
|
||||||
import { unified } from 'unified';
|
import { unified } from 'unified';
|
||||||
import markdown from 'remark-parse';
|
import markdown from 'remark-parse';
|
||||||
import markdownToHtml from 'remark-rehype';
|
import markdownToHtml from 'remark-rehype';
|
||||||
import rehypeStringify from 'rehype-stringify';
|
import rehypeStringify from 'rehype-stringify';
|
||||||
import remarkSlug from 'remark-slug';
|
import matter from 'gray-matter';
|
||||||
import matter from './gray-matter/index.js';
|
|
||||||
|
|
||||||
export { AstroMarkdownOptions, MarkdownRenderingOptions };
|
export { AstroMarkdownOptions, MarkdownRenderingOptions };
|
||||||
|
|
||||||
|
@ -24,29 +25,29 @@ export async function renderMarkdownWithFrontmatter(contents: string, opts?: Mar
|
||||||
return { ...value, frontmatter };
|
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 */
|
/** Shared utility for rendering markdown */
|
||||||
export async function renderMarkdown(content: string, opts?: MarkdownRenderingOptions | null) {
|
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();
|
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()
|
let parser = unified()
|
||||||
.use(markdown)
|
.use(markdown)
|
||||||
.use(remarkSlug)
|
.use([remarkJsx])
|
||||||
.use([remarkExpressions, { addResult: true }]);
|
.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 loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
|
||||||
const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins));
|
const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins));
|
||||||
|
|
||||||
|
@ -54,25 +55,25 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
|
||||||
parser.use(plugin, opts);
|
parser.use(plugin, opts);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (scopedClassName) {
|
// if (scopedClassName) {
|
||||||
parser.use(scopedStyles(scopedClassName));
|
// parser.use(scopedStyles(scopedClassName));
|
||||||
}
|
// }
|
||||||
|
|
||||||
parser.use(remarkCodeBlock);
|
parser.use(remarkCodeBlock);
|
||||||
parser.use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw', 'mdxTextExpression'] });
|
parser.use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw', 'mdxTextExpression', 'mdxJsxTextElement', 'mdxJsxFlowElement']});
|
||||||
parser.use(rehypeExpressions);
|
|
||||||
|
|
||||||
loadedRehypePlugins.forEach(([plugin, opts]) => {
|
loadedRehypePlugins.forEach(([plugin, opts]) => {
|
||||||
parser.use(plugin, opts);
|
parser.use(plugin, opts);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
parser.use(rehypeJsx).use(rehypeExpressions)
|
||||||
|
|
||||||
let result: string;
|
let result: string;
|
||||||
try {
|
try {
|
||||||
const vfile = await parser
|
const vfile = await parser
|
||||||
.use(raw)
|
|
||||||
.use(rehypeCollectHeaders)
|
.use(rehypeCollectHeaders)
|
||||||
.use(rehypeCodeBlock)
|
.use(rehypeCodeBlock)
|
||||||
.use(rehypeStringify, { entities: { useNamedReferences: true } })
|
.use(rehypeStringify, { allowParseErrors: true, preferUnquoted: true, allowDangerousHtml: true })
|
||||||
.process(content);
|
.process(content);
|
||||||
result = vfile.toString();
|
result = vfile.toString();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -80,7 +81,9 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
astro: { headers, source: content, html: result.toString() },
|
metadata: { headers, source: content, html: result.toString() },
|
||||||
content: result.toString(),
|
code: result.toString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default renderMarkdownWithFrontmatter;
|
27
packages/markdown/remark/src/rehype-jsx.ts
Normal file
27
packages/markdown/remark/src/rehype-jsx.ts
Normal 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;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
31
packages/markdown/remark/src/remark-jsx.ts
Normal file
31
packages/markdown/remark/src/remark-jsx.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
34
packages/markdown/remark/src/remark-slug.ts
Normal file
34
packages/markdown/remark/src/remark-slug.ts
Normal 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 GitHub’s 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;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,12 +4,8 @@ export type UnifiedPluginImport = Promise<{ default: unified.Plugin }>;
|
||||||
export type Plugin = string | [string, any] | UnifiedPluginImport | [UnifiedPluginImport, any];
|
export type Plugin = string | [string, any] | UnifiedPluginImport | [UnifiedPluginImport, any];
|
||||||
|
|
||||||
export interface AstroMarkdownOptions {
|
export interface AstroMarkdownOptions {
|
||||||
/** Enable or disable footnotes syntax extension */
|
remarkPlugins?: Plugin[];
|
||||||
footnotes: boolean;
|
rehypePlugins?: Plugin[];
|
||||||
/** Enable or disable GitHub-flavored Markdown syntax extension */
|
|
||||||
gfm: boolean;
|
|
||||||
remarkPlugins: Plugin[];
|
|
||||||
rehypePlugins: Plugin[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarkdownRenderingOptions extends Partial<AstroMarkdownOptions> {
|
export interface MarkdownRenderingOptions extends Partial<AstroMarkdownOptions> {
|
Loading…
Reference in a new issue