Add Markdown support
This commit is contained in:
parent
561fecd03b
commit
2cf885ee73
43 changed files with 1018 additions and 832 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 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>
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
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: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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -39,8 +39,9 @@
|
|||
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^0.1.0-canary.36",
|
||||
"@astrojs/compiler": "^0.1.0-canary.37",
|
||||
"@astrojs/language-server": "^0.7.16",
|
||||
"@astrojs/markdown-remark": "^0.3.1",
|
||||
"@astrojs/markdown-support": "0.3.1",
|
||||
"@astrojs/prism": "0.2.2",
|
||||
"@astrojs/renderer-preact": "0.2.2",
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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 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({}),
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
// 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') {
|
||||
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.
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import '@vite/client';
|
||||
import '/@vite/client';
|
||||
|
||||
if (import.meta.hot) {
|
||||
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 { 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
|
||||
|
@ -168,41 +169,71 @@ export async function ssr({ astroConfig, filePath, logging, mode, origin, pathna
|
|||
pageProps = { ...matchedStaticPath.props } || {};
|
||||
}
|
||||
|
||||
// 3. render page
|
||||
if (!browserHash && (viteServer as any)._optimizeDepsMetadata?.browserHash) browserHash = (viteServer as any)._optimizeDepsMetadata.browserHash; // note: this is "private" and may change over time
|
||||
const fullURL = new URL(pathname, origin);
|
||||
// 3. render page
|
||||
if (!browserHash && (viteServer as any)._optimizeDepsMetadata?.browserHash) browserHash = (viteServer as any)._optimizeDepsMetadata.browserHash; // note: this is "private" and may change over time
|
||||
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}`);
|
||||
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
|
||||
const Component = await mod.default;
|
||||
const ext = path.posix.extname(filePath.pathname);
|
||||
if (!Component)
|
||||
throw new Error(`Expected an exported Astro component but recieved typeof ${typeof Component}`);
|
||||
|
||||
let html = await renderPage(
|
||||
{
|
||||
styles: new Set(),
|
||||
scripts: new Set(),
|
||||
/** This function returns the `Astro` faux-global */
|
||||
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 };
|
||||
},
|
||||
_metadata: { importedModules, renderers },
|
||||
},
|
||||
Component,
|
||||
{},
|
||||
null
|
||||
);
|
||||
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
|
||||
|
||||
// 4. modify response
|
||||
if (mode === 'development') {
|
||||
// inject Astro HMR code
|
||||
html = injectAstroHMR(html);
|
||||
// inject Vite HMR code
|
||||
html = injectViteClient(html);
|
||||
// replace client hydration scripts
|
||||
html = resolveNpmImports(html);
|
||||
const result = {
|
||||
styles: new Set(),
|
||||
scripts: new Set(),
|
||||
/** This function returns the `Astro` faux-global */
|
||||
createAstro: (props: any) => {
|
||||
const site = new URL(origin);
|
||||
const url = new URL('.' + pathname, site);
|
||||
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 }
|
||||
},
|
||||
}, ChildComponent, {}, null);
|
||||
}))
|
||||
return contents;
|
||||
}
|
||||
}
|
||||
|
||||
let html = await renderPage(result, Component, {}, null);
|
||||
|
||||
// 4. modify response
|
||||
if (mode === 'development') {
|
||||
// inject Astro HMR code
|
||||
html = injectAstroHMR(html);
|
||||
// inject Vite HMR code
|
||||
html = injectViteClient(html);
|
||||
// replace client hydration scripts
|
||||
html = resolveNpmImports(html);
|
||||
}
|
||||
|
||||
// 5. finish
|
||||
return html;
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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,33 +14,42 @@ 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: don’t 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;
|
||||
|
||||
// 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' });
|
||||
// 2. Compile `.ts` to `.js`
|
||||
const { code, map } = await esbuild.transform(tsResult.code, { loader: 'ts', sourcemap: 'inline', sourcefile: id });
|
||||
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.
|
||||
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 });
|
||||
|
||||
return {
|
||||
code,
|
||||
map,
|
||||
};
|
||||
return {
|
||||
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) {
|
||||
|
|
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",
|
||||
"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"
|
||||
}
|
||||
}
|
|
@ -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, '{') };
|
||||
}
|
||||
return child;
|
||||
});
|
|
@ -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;
|
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 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> {
|
Loading…
Reference in a new issue