Compare commits
3 commits
main
...
experiment
Author | SHA1 | Date | |
---|---|---|---|
|
a2dca4675d | ||
|
405fe86f85 | ||
|
f329001af0 |
36 changed files with 1526 additions and 408 deletions
|
@ -9,203 +9,7 @@ import Markdown from 'astro/components/Markdown.astro';
|
|||
<body>
|
||||
<main>
|
||||
<Markdown>
|
||||
# 🍱 Collections
|
||||
|
||||
## ❓ What are Collections?
|
||||
|
||||
[Fetching data is easy in Astro][docs-data]. But what if you wanted to make a paginated blog? What if you wanted an easy way to sort data, or filter data based on part of the URL? Or generate an RSS 2.0 feed? When you need something a little more powerful than simple data fetching, Astro’s Collections API may be what you need.
|
||||
|
||||
An Astro Collection is similar to the general concept of Collections in static site generators like Jekyll, Hugo, Eleventy, etc. It’s a general way to load an entire data set. But one big difference between Astro Collections and traditional static site generators is: **Astro lets you seamlessly blend remote API data and local files in a JAMstack-friendly way.** To see how, this guide will walk through a few examples. If you’d like, you can reference the [blog example project][example-blog] to see the finished code in context.
|
||||
|
||||
## 🧑🎨 How to Use
|
||||
|
||||
By default, any Astro component can fetch data from any API or local `*.md` files. But what if you had a blog you wanted to paginate? What if you wanted to generate dynamic URLs based on metadata (e.g. `/tag/:tag/`)? Or do both together? Astro Collections are a way to do all of that. It’s perfect for generating blog-like content, or scaffolding out dynamic URLs from your data.
|
||||
|
||||
Let’s pretend we have some blog posts written already. This is our starting project structure:
|
||||
|
||||
```
|
||||
└── src/
|
||||
└── pages/
|
||||
└── post/
|
||||
└── (blog content)
|
||||
```
|
||||
|
||||
The first step in adding some dynamic collections is deciding on a URL schema. For our example website, we’re aiming for the following URLs:
|
||||
|
||||
- `/post/:post`: A single blog post page
|
||||
- `/posts/:page`: A list page of all blog posts, paginated, and sorted most recent first
|
||||
- `/tag/:tag`: All blog posts, filtered by a specific tag
|
||||
|
||||
Because `/post/:post` references the static files we have already, that doesn’t need to be a collection. But we will need collections for `/posts/:page` and `/tag/:tag` because those will be dynamically generated. For both collections we’ll create a `/src/pages/$[collection].astro` file. This is our new structure:
|
||||
|
||||
```diff
|
||||
└── src/
|
||||
└── pages/
|
||||
├── post/
|
||||
│ └── (blog content)
|
||||
+ ├── $posts.astro -> /posts/1, /posts/2, …
|
||||
+ └── $tag.astro -> /tag/:tag/1, /tag/:tag/2, …
|
||||
```
|
||||
|
||||
💁 **Tip**: Any `.astro` filename beginning with a `$` is how it’s marked as a collection.
|
||||
|
||||
In each `$[collection].astro` file, we’ll need 2 things:
|
||||
|
||||
```js
|
||||
// 1. We need to mark “collection” as a prop (this is a special reserved name)
|
||||
export let collection: any;
|
||||
|
||||
// 2. We need to export an async createCollection() function that will retrieve our data.
|
||||
export async function createCollection() {
|
||||
return {
|
||||
async data() {
|
||||
// return data here to load (we’ll cover how later)
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
These are important so your data is exposed to the page as a prop, and also Astro has everything it needs to gather your data and generate the proper routes. How it does this is more clear if we walk through a practical example.
|
||||
|
||||
#### Example 1: Simple pagination
|
||||
|
||||
Our blog posts all contain `title`, `tags`, and `published_at` in their frontmatter:
|
||||
|
||||
```md
|
||||
---
|
||||
title: My Blog Post
|
||||
tags:
|
||||
- javascript
|
||||
published_at: 2021-03-01 09:34:00
|
||||
---
|
||||
|
||||
# My Blog post
|
||||
|
||||
…
|
||||
```
|
||||
|
||||
There’s nothing special or reserved about any of these names; you’re free to name everything whatever you’d like, or have as much or little frontmatter as you need.
|
||||
|
||||
```jsx
|
||||
// /src/pages/$posts.astro
|
||||
---
|
||||
export let collection: any;
|
||||
|
||||
export async function createCollection() {
|
||||
const allPosts = Astro.fetchContent('./post/*.md'); // load data that already lives at `/post/:slug`
|
||||
allPosts.sort((a, b) => new Date(b.published_at) - new Date(a.published_at)); // sort newest -> oldest (we got "published_at" from frontmatter!)
|
||||
|
||||
// (load more data here, if needed)
|
||||
|
||||
return {
|
||||
async data() {
|
||||
return allPosts;
|
||||
},
|
||||
pageSize: 10, // how many we want to show per-page (default: 25)
|
||||
};
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
return new Date(date).toUTCString();
|
||||
}
|
||||
---
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Blog Posts: page {collection.page.current}</title>
|
||||
<link rel="canonical" href={collection.url.current} />
|
||||
<link rel="prev" href={collection.url.prev} />
|
||||
<link rel="next" href={collection.url.next} />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h5>Results {collection.start + 1}–{collection.end + 1} of {collection.total}</h6>
|
||||
{collection.data.map((post) => (
|
||||
<h1>{post.title}</h1>
|
||||
<time>{formatDate(post.published_at)}</time>
|
||||
<a href={post.url}>Read</a>
|
||||
)}
|
||||
</main>
|
||||
<footer>
|
||||
<h4>Page {collection.page.current} / {collection.page.last}</h4>
|
||||
<nav class="nav">
|
||||
<a class="prev" href={collection.url.prev || '#'}>Prev</a>
|
||||
<a class="next" href={collection.url.next || '#'}>Next</a>
|
||||
</nav>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Let’s walk through some of the key parts:
|
||||
|
||||
- `export let collection`: this is important because it exposes a prop to the page for Astro to return with all your data loaded. ⚠️ **It must be named `collection`**.
|
||||
- `export async function createCollection()`: this is also required, **and must be named this exactly.** This is an async function that lets you load data from anywhere (even a remote API!). At the end, you must return an object with `{ data: yourData }`. There are other options such as `pageSize` we’ll cover later.
|
||||
- `{collection.data.map((post) => (…`: this lets us iterate over all the markdown posts. This will take the shape of whatever you loaded in `createCollection()`. It will always be an array.
|
||||
- `{collection.page.current}`: this, and other properties, simply return more info such as what page a user is on, what the URL is, etc. etc.
|
||||
- Curious about everything on `collection`? See the [reference][collection-api].
|
||||
|
||||
#### Example 2: Advanced filtering & pagination
|
||||
|
||||
In our earlier example, we covered simple pagination for `/posts/1`, but we’d still like to make `/tag/:tag/1` and `/year/:year/1`. To do that, we’ll create 2 more collections: `/src/pages/$tag.astro` and `src/pages/$year.astro`. Assume that the markup is the same, but we’ve expanded the `createCollection()` function with more data.
|
||||
|
||||
```diff
|
||||
// /src/pages/$tag.astro
|
||||
---
|
||||
import Pagination from '../components/Pagination.astro';
|
||||
import PostPreview from '../components/PostPreview.astro';
|
||||
|
||||
export let collection: any;
|
||||
|
||||
export async function createCollection() {
|
||||
const allPosts = Astro.fetchContent('./post/*.md');
|
||||
allPosts.sort((a, b) => new Date(b.published_at) - new Date(a.published_at));
|
||||
+ const allTags = [...new Set(allPosts.map((post) => post.tags).flat())]; // gather all unique tags (we got "tags" from frontmatter!)
|
||||
+ allTags.sort((a, b) => a.localeCompare(b)); // sort tags A -> Z
|
||||
+ const routes = allTags.map((tag) => ({ tag })); // this is where we set { params: { tag } }
|
||||
|
||||
return {
|
||||
- async data() {
|
||||
- return allPosts;
|
||||
+ async data({ params }) {
|
||||
+ return allPosts.filter((post) => post.tags.includes(params.tag)); // filter posts that match the :tag from the URL ("params")
|
||||
},
|
||||
pageSize: 10,
|
||||
+ routes,
|
||||
+ permalink: ({ params }) => `/tag/${params.tag}/` // this is where we generate our URL structure
|
||||
};
|
||||
}
|
||||
---
|
||||
```
|
||||
|
||||
Some important concepts here:
|
||||
|
||||
- `routes = allTags.map((tag) => ({ tag }))`: Astro handles pagination for you automatically. But when it needs to generate multiple routes, this is where you tell Astro about all the possible routes. This way, when you run `astro build`, your static build isn’t missing any pages.
|
||||
- `` permalink: ({ params }) => `/tag/${params.tag}/` ``: this is where you tell Astro what the generated URL should be. Note that while you have control over this, the root of this must match the filename (it's best **NOT** to use `/pages/$tag.astro` to generate `/year/$year.astro`; that should live at `/pages/$year.astro` as a separate file).
|
||||
- `allPosts.filter((post) => post.tag === params.tag)`: we aren’t returning all posts here; we’re only returning posts with a matching tag. _What tag,_ you ask? The `routes` array has `[{ tag: 'javascript' }, { tag: '…`, and all the routes we need to gather. So we first need to query everything, but only return the `.filter()`ed posts at the very end.
|
||||
|
||||
Other things of note is that we are sorting like before, but we filter by the frontmatter `tag` property, and return those at URLs.
|
||||
|
||||
These are still paginated, too! But since there are other conditions applied, they live at a different URL.
|
||||
|
||||
#### Tips
|
||||
|
||||
- Having to load different collections in different `$[collection].astro` files might seem like a pain at first, until you remember **you can create reusable components!** Treat `/pages/*.astro` files as your one-off routing & data fetching logic, and treat `/components/*.astro` as your reusable markup. If you find yourself duplicating things too much, you can probably use a component instead!
|
||||
- Stay true to `/pages/$[collection].astro` naming. If you have an `/all-posts/*` route, then use `/pages/$all-posts.astro` to manage that. Don’t try and trick `permalink` to generate too many URL trees; it’ll only result in pages being missed when it comes time to build.
|
||||
|
||||
### 📚 Further Reading
|
||||
|
||||
- [Fetching data in Astro][docs-data]
|
||||
- API Reference: [collection][collection-api]
|
||||
- API Reference: [createCollection()][create-collection-api]
|
||||
- API Reference: [Creating an RSS feed][create-collection-api]
|
||||
|
||||
[docs-data]: ../README.md#-fetching-data
|
||||
[collection-api]: ./api.md#collection
|
||||
[create-collection-api]: ./api.md#createcollection
|
||||
[example-blog]: ../examples/blog
|
||||
[fetch-content]: ./api.md#fetchcontent
|
||||
</Markdown>
|
||||
</Markdown>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -17,7 +17,7 @@ const items = ['A', 'B', 'C'];
|
|||
|
||||
**Astro Markdown** brings native Markdown support to HTML!
|
||||
|
||||
> It's inspired by [`mdx`](https://mdxjs.com/) and powered by [`remark`](https://github.com/remarkjs/remark)).
|
||||
> It's inspired by [`mdx`](https://mdxjs.com/) and powered by [`remark`](https://github.com/remarkjs/remark).
|
||||
|
||||
The best part? It comes with all the Astro features you expect.
|
||||
|
||||
|
|
2
examples/doc/.vscode/settings.json
vendored
Normal file
2
examples/doc/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
{
|
||||
}
|
5
examples/doc/astro.config.mjs
Normal file
5
examples/doc/astro.config.mjs
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
extensions: {
|
||||
'.tsx': 'preact'
|
||||
}
|
||||
};
|
17
examples/doc/package.json
Normal file
17
examples/doc/package.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "@example/doc",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"astro-dev": "nodemon --delay 0.5 -w ../../packages/astro/dist -x '../../packages/astro/astro.mjs dev'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "^0.10.0",
|
||||
"nodemon": "^2.0.7"
|
||||
},
|
||||
"snowpack": {
|
||||
"workspaceRoot": "../.."
|
||||
}
|
||||
}
|
155
examples/doc/public/code.css
Normal file
155
examples/doc/public/code.css
Normal file
|
@ -0,0 +1,155 @@
|
|||
.language-css > code,
|
||||
.language-sass > code,
|
||||
.language-scss > code {
|
||||
color: #fd9170;
|
||||
}
|
||||
|
||||
[class*="language-"] .namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.token.atrule {
|
||||
color: #c792ea;
|
||||
}
|
||||
|
||||
.token.attr-name {
|
||||
color: #ffcb6b;
|
||||
}
|
||||
|
||||
.token.attr-value {
|
||||
color: #a5e844;
|
||||
}
|
||||
|
||||
.token.attribute {
|
||||
color: #a5e844;
|
||||
}
|
||||
|
||||
.token.boolean {
|
||||
color: #c792ea;
|
||||
}
|
||||
|
||||
.token.builtin {
|
||||
color: #ffcb6b;
|
||||
}
|
||||
|
||||
.token.cdata {
|
||||
color: #80cbc4;
|
||||
}
|
||||
|
||||
.token.char {
|
||||
color: #80cbc4;
|
||||
}
|
||||
|
||||
.token.class {
|
||||
color: #ffcb6b;
|
||||
}
|
||||
|
||||
.token.class-name {
|
||||
color: #f2ff00;
|
||||
}
|
||||
|
||||
.token.comment {
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.token.constant {
|
||||
color: #c792ea;
|
||||
}
|
||||
|
||||
.token.deleted {
|
||||
color: #ff6666;
|
||||
}
|
||||
|
||||
.token.doctype {
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
color: #ff6666;
|
||||
}
|
||||
|
||||
.token.function {
|
||||
color: #c792ea;
|
||||
}
|
||||
|
||||
.token.hexcode {
|
||||
color: #f2ff00;
|
||||
}
|
||||
|
||||
.token.id {
|
||||
color: #c792ea;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.important {
|
||||
color: #c792ea;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.inserted {
|
||||
color: #80cbc4;
|
||||
}
|
||||
|
||||
.token.keyword {
|
||||
color: #c792ea;
|
||||
}
|
||||
|
||||
.token.number {
|
||||
color: #fd9170;
|
||||
}
|
||||
|
||||
.token.operator {
|
||||
color: #89ddff;
|
||||
}
|
||||
|
||||
.token.prolog {
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.token.property {
|
||||
color: #80cbc4;
|
||||
}
|
||||
|
||||
.token.pseudo-class {
|
||||
color: #a5e844;
|
||||
}
|
||||
|
||||
.token.pseudo-element {
|
||||
color: #a5e844;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #89ddff;
|
||||
}
|
||||
|
||||
.token.regex {
|
||||
color: #f2ff00;
|
||||
}
|
||||
|
||||
.token.selector {
|
||||
color: #ff6666;
|
||||
}
|
||||
|
||||
.token.string {
|
||||
color: #a5e844;
|
||||
}
|
||||
|
||||
.token.symbol {
|
||||
color: #c792ea;
|
||||
}
|
||||
|
||||
.token.tag {
|
||||
color: #ff6666;
|
||||
}
|
||||
|
||||
.token.unit {
|
||||
color: #fd9170;
|
||||
}
|
||||
|
||||
.token.url {
|
||||
color: #ff6666;
|
||||
}
|
||||
|
||||
.token.variable {
|
||||
color: #ff6666;
|
||||
}
|
11
examples/doc/public/favicon.svg
Normal file
11
examples/doc/public/favicon.svg
Normal file
|
@ -0,0 +1,11 @@
|
|||
<svg width="256" height="256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
#flame { fill: #FF5D01; }
|
||||
#a { fill: #000014; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#a { fill: #fff; }
|
||||
}
|
||||
</style>
|
||||
<path id="a" fill-rule="evenodd" clip-rule="evenodd" d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z" />
|
||||
<path id="flame" fill-rule="evenodd" clip-rule="evenodd" d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
350
examples/doc/public/index.css
Normal file
350
examples/doc/public/index.css
Normal file
|
@ -0,0 +1,350 @@
|
|||
@import './theme';
|
||||
@import './code';
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--user-font-scale: 1rem - 16px;
|
||||
--max-width: calc(100% - 2rem);
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
:root {
|
||||
--max-width: 48em;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
font-family: var(--font-body);
|
||||
font-size: 1rem;
|
||||
font-size: clamp(0.875rem, 0.4626rem + 1.0309vw + var(--user-font-scale), 1.125rem);
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content main > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
:is(h1, h2, h3, h4, h5, h6) {
|
||||
margin-bottom: 1.38rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
:is(h1, h2) {
|
||||
max-width: 40ch;
|
||||
}
|
||||
|
||||
:is(h2, h3):not(:first-child) {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.488rem, 1.9240rem + 1.4100vw, 3.052rem);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: clamp(2.074rem, 1.7070rem + 0.9175vw, 2.441rem);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: clamp(1.728rem, 1.5030rem + 0.5625vw, 1.953rem);
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: clamp(1.44rem, 1.3170rem + 0.3075vw, 1.563rem);
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: clamp(1.2rem, 1.1500rem + 0.1250vw, 1.25rem);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--theme-text-light);
|
||||
}
|
||||
|
||||
small, .text_small {
|
||||
font-size: 0.833rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--theme-accent);
|
||||
font-weight: 400;
|
||||
text-underline-offset: 0.08em;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
a > code:not([class*="language"]) {
|
||||
position: relative;
|
||||
color: var(--theme-accent);
|
||||
background: transparent;
|
||||
text-underline-offset: var(--padding-block);
|
||||
}
|
||||
|
||||
a > code:not([class*="language"])::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
background: var(--theme-accent);
|
||||
opacity: var(--theme-accent-opacity);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 0.25em;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Supporting Content */
|
||||
|
||||
code:not([class*="language"]) {
|
||||
--border-radius: 3px;
|
||||
--padding-block: 0.2rem;
|
||||
--padding-inline: 0.33rem;
|
||||
|
||||
font-family: var(--font-mono);
|
||||
font-size: .85em;
|
||||
color: inherit;
|
||||
background-color: var(--theme-code-inline-bg);
|
||||
padding: var(--padding-block) var(--padding-inline);
|
||||
margin: calc(var(--padding-block) * -1) -0.125em;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
pre > code:not([class*="language"]) {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
background-color: var(--theme-code-bg);
|
||||
color: var(--theme-code-text);
|
||||
--padding-block: 1rem;
|
||||
--padding-inline: 2rem;
|
||||
padding: var(--padding-block) var(--padding-inline);
|
||||
padding-right: calc(var(--padding-inline) * 2);
|
||||
margin-left: calc(50vw - var(--padding-inline));
|
||||
transform: translateX(-50vw);
|
||||
|
||||
line-height: 1.414;
|
||||
width: calc(100vw + 4px);
|
||||
max-width: calc(100% + (var(--padding-inline) * 2));
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 37.75em) {
|
||||
pre {
|
||||
--padding-inline: 1.25rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 2rem 0;
|
||||
padding: 0.5em 1rem;
|
||||
border-left: 3px solid rgba(0, 0, 0, 0.35);
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 0 0.25rem 0.25rem 0;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header button {
|
||||
background-color: var(--theme-bg);
|
||||
}
|
||||
header button:hover,
|
||||
header button:focus {
|
||||
background: var(--theme-text);
|
||||
color: var(--theme-bg);
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
gap: 0.25em;
|
||||
padding: 0.33em 0.67em;
|
||||
border: 0;
|
||||
background: var(--theme-bg);
|
||||
display: flex;
|
||||
font-size: 1rem;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
border-radius: 99em;
|
||||
background-color: var(--theme-bg);
|
||||
}
|
||||
button:hover {
|
||||
|
||||
}
|
||||
|
||||
#theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
padding: 0.33em 0.67em;
|
||||
margin-left: -0.67em;
|
||||
margin-right: -0.67em;
|
||||
border-radius: 99em;
|
||||
background-color: var(--theme-bg);
|
||||
}
|
||||
#theme-toggle:focus-within {
|
||||
outline: 2px solid transparent;
|
||||
box-shadow: 0 0 0 0.08em var(--theme-accent), 0 0 0 0.12em white;
|
||||
}
|
||||
|
||||
#theme-toggle > label {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
opacity: 0.5;
|
||||
transition: transform 120ms ease-out, opacity 120ms ease-out;
|
||||
}
|
||||
|
||||
#theme-toggle > label:hover,
|
||||
#theme-toggle > label:focus {
|
||||
transform: scale(1.125);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#theme-toggle .checked {
|
||||
color: var(--theme-accent);
|
||||
transform: scale(1.125);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input[name="theme-toggle"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
nav h4 {
|
||||
font-weight: 400;
|
||||
font-size: 1.25rem;
|
||||
margin: 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.edit-on-github,
|
||||
.header-link {
|
||||
font-size: 1rem;
|
||||
padding-left: 1rem;
|
||||
border-left: 4px solid var(--theme-divider);
|
||||
}
|
||||
|
||||
.edit-on-github:hover,
|
||||
.edit-on-github:focus,
|
||||
.header-link:hover,
|
||||
.header-link:focus {
|
||||
color: var(--theme-text-light);
|
||||
border-left-color: var(--theme-text-lighter);
|
||||
}
|
||||
|
||||
.header-link:focus-within {
|
||||
color: var(--theme-text-light);
|
||||
border-left-color: var(--theme-text-lighter);
|
||||
}
|
||||
|
||||
.header-link.active {
|
||||
border-left-color: var(--theme-accent);
|
||||
color: var(--theme-accent);
|
||||
}
|
||||
|
||||
.header-link.depth-2 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-link.depth-3 {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
.header-link.depth-4 {
|
||||
padding-left: 3rem;
|
||||
}
|
||||
|
||||
.edit-on-github,
|
||||
.header-link a {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.edit-on-github {
|
||||
margin-top: 2rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
.edit-on-github > * {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0;
|
||||
transform: translateX(0);
|
||||
transition: 120ms transform ease-out;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link:focus {
|
||||
color: var(--theme-text-lighter);
|
||||
transform: translateX(0.25em);
|
||||
}
|
||||
|
||||
.nav-link:focus-within {
|
||||
color: var(--theme-text-lighter);
|
||||
transform: translateX(0.25em);
|
||||
}
|
||||
|
||||
.nav-link a {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-groups > li + li {
|
||||
margin-top: 2rem;
|
||||
}
|
72
examples/doc/public/theme.css
Normal file
72
examples/doc/public/theme.css
Normal file
|
@ -0,0 +1,72 @@
|
|||
:root {
|
||||
--font-body: system-ui;
|
||||
--font-mono: source-code-pro,Menlo,Monaco,Consolas,'Courier New',monospace;
|
||||
|
||||
--color-white: #FFF;
|
||||
--color-black: #000014;
|
||||
|
||||
--color-gray-50: #F9FAFB;
|
||||
--color-gray-100: #F3F4F6;
|
||||
--color-gray-200: #E5E7EB;
|
||||
--color-gray-300: #D1D5DB;
|
||||
--color-gray-400: #9CA3AF;
|
||||
--color-gray-500: #6B7280;
|
||||
--color-gray-600: #4B5563;
|
||||
--color-gray-700: #374151;
|
||||
--color-gray-800: #1F2937;
|
||||
--color-gray-900: #111827;
|
||||
|
||||
--color-blue: #3894FF;
|
||||
--color-blue-rgb: 56,148,255;
|
||||
--color-green: #17C083;
|
||||
--color-green-rgb: 23,192,131;
|
||||
--color-orange: #FF5D01;
|
||||
--color-orange-rgb: 255,93,1;
|
||||
--color-purple: #882DE7;
|
||||
--color-purple-rgb: 136,45,231;
|
||||
--color-red: #FF1639;
|
||||
--color-red-rgb: 255,22,57;
|
||||
--color-yellow: #FFBE2D;
|
||||
--color-yellow-rgb: 255,190,45;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--theme-accent: var(--color-blue);
|
||||
--theme-accent-rgb: var(--color-blue-rgb);
|
||||
--theme-accent-opacity: 0.1;
|
||||
--theme-divider: var(--color-gray-100);
|
||||
--theme-text: var(--color-gray-800);
|
||||
--theme-text-light: var(--color-gray-600);
|
||||
--theme-text-lighter: var(--color-gray-400);
|
||||
--theme-bg: var(--color-white);
|
||||
--theme-bg-offset: var(--color-gray-100);
|
||||
--theme-bg-accent: rgba(var(--theme-accent-rgb), var(--theme-accent-opacity));
|
||||
--theme-code-inline-bg: var(--color-gray-100);
|
||||
--theme-code-text: var(--color-gray-100);
|
||||
--theme-code-bg: var(--color-gray-700);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--theme-bg);
|
||||
color: var(--theme-text);
|
||||
}
|
||||
|
||||
:root.theme-dark {
|
||||
color-scheme: dark;
|
||||
--theme-accent-opacity: 0.3;
|
||||
--theme-divider: var(--color-gray-900);
|
||||
--theme-text: var(--color-gray-200);
|
||||
--theme-text-light: var(--color-gray-400);
|
||||
--theme-text-lighter: var(--color-gray-600);
|
||||
--theme-bg: var(--color-black);
|
||||
--theme-bg-offset: var(--color-gray-900);
|
||||
--theme-code-inline-bg: var(--color-gray-800);
|
||||
--theme-code-text: var(--color-gray-200);
|
||||
--theme-code-bg: var(--color-gray-900);
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: var(--theme-accent);
|
||||
background-color: rgba(var(--theme-accent-rgb), var(--theme-accent-opacity));
|
||||
}
|
8
examples/doc/public/theme.js
Normal file
8
examples/doc/public/theme.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
(() => {
|
||||
const root = document.documentElement;
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
root.classList.add('theme-dark')
|
||||
} else {
|
||||
root.classList.remove('theme-dark')
|
||||
}
|
||||
})()
|
15
examples/doc/src/components/ArticleFooter.astro
Normal file
15
examples/doc/src/components/ArticleFooter.astro
Normal file
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
import AvatarList from './AvatarList.astro';
|
||||
---
|
||||
|
||||
<footer>
|
||||
<AvatarList />
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
footer {
|
||||
margin-top: 4rem;
|
||||
padding: 2rem 0;
|
||||
border-top: 3px solid var(--theme-divider);
|
||||
}
|
||||
</style>
|
72
examples/doc/src/components/AvatarList.astro
Normal file
72
examples/doc/src/components/AvatarList.astro
Normal file
|
@ -0,0 +1,72 @@
|
|||
<ul class="avatar-list">
|
||||
<li><a href="#smol-avatar-list"><img alt="Avatar 1" width="64" height="64" src='https://avataaars.io/?avatarStyle=Transparent&topType=LongHairBun&accessoriesType=Blank&hairColor=Auburn&facialHairType=BeardMedium&facialHairColor=Auburn&clotheType=ShirtCrewNeck&clotheColor=Blue01&eyeType=Side&eyebrowType=RaisedExcitedNatural&mouthType=Serious&skinColor=Tanned' /></a></li>
|
||||
<li><a href="#smol-avatar-list"><img alt="Avatar 2" width="64" height="64" src='https://avataaars.io/?avatarStyle=Transparent&topType=LongHairDreads&accessoriesType=Blank&hairColor=Brown&facialHairType=Blank&clotheType=ShirtScoopNeck&clotheColor=PastelGreen&eyeType=Default&eyebrowType=DefaultNatural&mouthType=Smile&skinColor=Tanned' /></a></li>
|
||||
<li><a href="#smol-avatar-list"><img alt="Avatar 3" width="64" height="64" src='https://avataaars.io/?avatarStyle=Transparent&topType=LongHairCurly&hairColor=BrownDark&facialHairType=Blank&clotheType=GraphicShirt&clotheColor=Pink&graphicType=Diamond&eyeType=Side&eyebrowType=Default&mouthType=Default&skinColor=Brown'/></a></li>
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
.avatar-list {
|
||||
--avatar-size: 2.5rem;
|
||||
--avatar-count: 3;
|
||||
|
||||
display: grid;
|
||||
list-style: none;
|
||||
/* Default to displaying most of the avatar to
|
||||
enable easier access on touch devices, ensuring
|
||||
the WCAG touch target size is met or exceeded */
|
||||
grid-template-columns: repeat(
|
||||
var(--avatar-count),
|
||||
max(44px, calc(var(--avatar-size) / 1.15))
|
||||
);
|
||||
/* `padding` matches added visual dimensions of
|
||||
the `box-shadow` to help create a more accurate
|
||||
computed component size */
|
||||
padding: 0.08em;
|
||||
font-size: var(--avatar-size);
|
||||
}
|
||||
|
||||
@media (any-hover: hover) and (any-pointer: fine) {
|
||||
.avatar-list {
|
||||
/* We create 1 extra cell to enable the computed
|
||||
width to match the final visual width */
|
||||
grid-template-columns: repeat(
|
||||
calc(var(--avatar-count) + 1),
|
||||
calc(var(--avatar-size) / 1.75)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-list li {
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
}
|
||||
|
||||
.avatar-list li:hover ~ li a,
|
||||
.avatar-list li:focus-within ~ li a {
|
||||
transform: translateX(33%);
|
||||
}
|
||||
|
||||
.avatar-list img,
|
||||
.avatar-list a {
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.avatar-list a {
|
||||
transition: transform 180ms ease-in-out;
|
||||
}
|
||||
|
||||
.avatar-list img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 0.05em #fff, 0 0 0 0.08em rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.avatar-list a:focus {
|
||||
outline: 2px solid transparent;
|
||||
/* Double-layer trick to work for dark and light backgrounds */
|
||||
box-shadow: 0 0 0 0.08em var(--theme-accent), 0 0 0 0.12em white;
|
||||
}
|
||||
</style>
|
55
examples/doc/src/components/DocSidebar.tsx
Normal file
55
examples/doc/src/components/DocSidebar.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import type { FunctionalComponent } from 'preact';
|
||||
import { h } from 'preact';
|
||||
import { useState, useEffect, useRef } from 'preact/hooks';
|
||||
import EditOnGithub from './EditOnGithub';
|
||||
|
||||
const DocSidebar: FunctionalComponent<{ headers: any[]; editHref: string; }> = ({ headers, editHref }) => {
|
||||
const itemOffsets = useRef([]);
|
||||
const [activeId, setActiveId] = useState<string>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const getItemOffsets = () => {
|
||||
const titles = document.querySelectorAll('article :is(h2, h3, h4)');
|
||||
itemOffsets.current = Array.from(titles).map(title => ({
|
||||
id: title.id,
|
||||
topOffset: title.getBoundingClientRect().top + window.scrollY
|
||||
}));
|
||||
}
|
||||
|
||||
const onScroll = () => {
|
||||
const itemIndex = itemOffsets.current.findIndex(item => item.topOffset > window.scrollY + (window.innerHeight / 3));
|
||||
if (itemIndex === 0) {
|
||||
setActiveId(undefined);
|
||||
} else if (itemIndex === -1) {
|
||||
setActiveId(itemOffsets.current[itemOffsets.current.length - 1].id)
|
||||
} else {
|
||||
setActiveId(itemOffsets.current[itemIndex - 1].id)
|
||||
}
|
||||
}
|
||||
|
||||
getItemOffsets();
|
||||
window.addEventListener('resize', getItemOffsets);
|
||||
window.addEventListener('scroll', onScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', getItemOffsets);
|
||||
window.removeEventListener('scroll', onScroll);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<nav>
|
||||
<div>
|
||||
<h4>Contents</h4>
|
||||
<ul>
|
||||
{headers.filter(({ depth }) => depth > 1 && depth < 5).map(header => <li class={`header-link depth-${header.depth} ${activeId === header.slug ? 'active' : ''}`.trim()}><a href={`#${header.slug}`}>{header.text}</a></li>)}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<EditOnGithub href={editHref} />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export default DocSidebar;
|
13
examples/doc/src/components/EditOnGithub.tsx
Normal file
13
examples/doc/src/components/EditOnGithub.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import type { FunctionalComponent } from 'preact';
|
||||
import { h } from 'preact';
|
||||
|
||||
const EditOnGithub: FunctionalComponent<{ href: string }> = ({ href }) => {
|
||||
return (
|
||||
<a class="edit-on-github" href={href}>
|
||||
<svg preserveAspectRatio="xMidYMid meet" height="1em" width="1em" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 438.549 438.549" stroke="none" class="icon-7f6730be--text-3f89f380"><g><path d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 0 1-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"></path></g></svg>
|
||||
<span>Edit on GitHub</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditOnGithub;
|
49
examples/doc/src/components/Note.astro
Normal file
49
examples/doc/src/components/Note.astro
Normal file
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
export let type = "tip";
|
||||
export let title;
|
||||
---
|
||||
|
||||
<aside class={`note type-${type}`}>
|
||||
{title && <label>{title}</label>}
|
||||
<slot />
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
.note {
|
||||
--padding-block: 1rem;
|
||||
--padding-inline: 1.25rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding: var(--padding-block) var(--padding-inline);
|
||||
margin-left: calc(var(--padding-inline) * -1);
|
||||
margin-right: calc(var(--padding-inline) * -1);
|
||||
|
||||
background: var(--theme-bg-offset);
|
||||
border-left: calc(var(--padding-inline) / 2) solid var(--color);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.note label {
|
||||
font-weight: 500;
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
/* .note :global(a) {
|
||||
color: var(--color);
|
||||
} */
|
||||
|
||||
.note.type-tip {
|
||||
--color: var(--color-green);
|
||||
--color-rgb: var(--color-green-rgb);
|
||||
}
|
||||
.note.type-warning {
|
||||
--color: var(--color-yellow);
|
||||
--color-rgb: var(--color-yellow-rgb);
|
||||
}
|
||||
.note.type-error {
|
||||
--color: var(--color-red);
|
||||
--color-rgb: var(--color-red-rgb);
|
||||
}
|
||||
</style>
|
20
examples/doc/src/components/SiteSidebar.astro
Normal file
20
examples/doc/src/components/SiteSidebar.astro
Normal file
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
import { sidebar } from '../config.ts';
|
||||
---
|
||||
|
||||
<nav>
|
||||
<ul class="nav-groups">
|
||||
{sidebar.map(category => (
|
||||
<li>
|
||||
<div class="nav-group">
|
||||
<h4 class="nav-group-title">{category.text}</h4>
|
||||
<ul>
|
||||
{category.children.map(child => (
|
||||
<li class="nav-link"><a href={child.link}>{child.text}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
65
examples/doc/src/components/ThemeToggle.tsx
Normal file
65
examples/doc/src/components/ThemeToggle.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import type { FunctionalComponent } from 'preact';
|
||||
import { h, Fragment } from 'preact';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
|
||||
const themes = [
|
||||
'system',
|
||||
'light',
|
||||
'dark'
|
||||
]
|
||||
|
||||
const icons = [
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2.22l.123.489.804.804A1 1 0 0113 18H7a1 1 0 01-.707-1.707l.804-.804L7.22 15H5a2 2 0 01-2-2V5zm5.771 7H5V5h10v7H8.771z" clip-rule="evenodd" />
|
||||
</svg>,
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
|
||||
</svg>,
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
||||
</svg>
|
||||
]
|
||||
|
||||
const ThemeToggle: FunctionalComponent = () => {
|
||||
const [theme, setTheme] = useState(themes[0]);
|
||||
|
||||
useEffect(() => {
|
||||
const user = localStorage.getItem('theme')
|
||||
if (!user) return;
|
||||
setTheme(user);
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (theme === 'system') {
|
||||
localStorage.removeItem('theme');
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
root.classList.add('theme-dark');
|
||||
} else {
|
||||
root.classList.remove('theme-dark');
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem('theme', theme);
|
||||
if (theme === 'light') {
|
||||
root.classList.remove('theme-dark');
|
||||
} else {
|
||||
root.classList.add('theme-dark');
|
||||
}
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
return <div id="theme-toggle">
|
||||
{themes.map((t, i) => {
|
||||
const icon = icons[i];
|
||||
const checked = t === theme;
|
||||
return (
|
||||
<label className={checked ? 'checked' : ''}>
|
||||
{icon}
|
||||
<input type="radio" name="theme-toggle" checked={checked} value={t} title={`Use ${t} theme`} aria-label={`Use ${t} theme`} onChange={() => setTheme(t)} />
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default ThemeToggle;
|
9
examples/doc/src/config.ts
Normal file
9
examples/doc/src/config.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export const sidebar = [
|
||||
{
|
||||
text: 'Introduction',
|
||||
children: [
|
||||
{ text: 'Welcome', link: '/' },
|
||||
{ text: 'Example', link: '/example' }
|
||||
]
|
||||
}
|
||||
]
|
221
examples/doc/src/layouts/Main.astro
Normal file
221
examples/doc/src/layouts/Main.astro
Normal file
|
@ -0,0 +1,221 @@
|
|||
---
|
||||
import ArticleFooter from '../components/ArticleFooter.astro';
|
||||
import SiteSidebar from '../components/SiteSidebar.astro';
|
||||
import ThemeToggle from '../components/ThemeToggle.tsx';
|
||||
import DocSidebar from '../components/DocSidebar.tsx';
|
||||
|
||||
export let content;
|
||||
const headers = content?.astro?.headers;
|
||||
let editHref = Astro?.request?.url?.pathname?.slice(1) ?? '';
|
||||
if (editHref === '') editHref = `index`;
|
||||
editHref = `https://github.com/snowpackjs/astro/tree/main/examples/doc/src/pages/${editHref}.md`
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>{content.title}</title>
|
||||
|
||||
<link rel="stylesheet" href="/index.css" />
|
||||
<script src="/theme.js" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: 3.5rem 1fr;
|
||||
--gutter: 0.5rem;
|
||||
--doc-padding: 2rem;
|
||||
}
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--theme-bg-offset);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: minmax(var(--gutter), 1fr) minmax(0, var(--max-width)) minmax(var(--gutter), 1fr);
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.menu-and-logo {
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
nav.layout {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
nav.layout :global(> :nth-child(1)) {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
#site-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
color: var(--theme-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#site-title:hover,
|
||||
#site-title:focus {
|
||||
color: var(--theme-text-light);
|
||||
}
|
||||
|
||||
#site-title h1 {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: 64ch;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.layout :global(> *) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
max-height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: var(--doc-padding) 0;
|
||||
}
|
||||
|
||||
#sidebar-nav {
|
||||
display: none;
|
||||
max-height: 100vh;
|
||||
padding: var(--doc-padding) 0;
|
||||
}
|
||||
|
||||
#article {
|
||||
padding: var(--doc-padding) var(--gutter);
|
||||
grid-column: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 64ch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#sidebar-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 64em) {
|
||||
.menu-and-logo button {
|
||||
display: none;
|
||||
}
|
||||
.layout {
|
||||
grid-template-columns: 20rem minmax(0, 1fr);
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
#article {
|
||||
grid-column: 2;
|
||||
}
|
||||
#sidebar-nav {
|
||||
display: flex;
|
||||
}
|
||||
#sidebar-content {
|
||||
/* display: flex; */
|
||||
grid-column: 3;
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 88em) {
|
||||
.layout {
|
||||
grid-template-columns: minmax(var(--gutter), 1fr) 20rem minmax(0, var(--max-width)) 16rem minmax(var(--gutter), 1fr);
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
#sidebar-nav,
|
||||
.nav-wrapper :global(:nth-child(1)) {
|
||||
grid-column: 2;
|
||||
}
|
||||
#article,
|
||||
.nav-wrapper :global(:nth-child(2)) {
|
||||
grid-column: 3;
|
||||
}
|
||||
#sidebar-content,
|
||||
.nav-wrapper :global(:nth-child(3)) {
|
||||
display: flex;
|
||||
grid-column: 4;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<nav class="layout">
|
||||
<div class="nav-wrapper">
|
||||
<div class="menu-and-logo flex">
|
||||
<button id="menu-toggle">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<a id="site-title" href="/">
|
||||
<svg width="1em" height="1em" viewBox="0 0 340 340" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M320 170C320 170 296.88 171.746 267.428 188.75C237.975 205.754 205.754 237.975 188.75 267.428C171.746 296.88 170 320 170 320C170 320 168.254 296.88 151.25 267.428C134.246 237.975 102.025 205.754 72.5721 188.75C43.1197 171.746 20 170 20 170C20 170 43.1197 168.254 72.5721 151.25C102.025 134.246 134.246 102.025 151.25 72.5721C168.254 43.1197 170 20 170 20C170 20 171.746 43.1197 188.75 72.5721C205.754 102.025 237.975 134.246 267.428 151.25C296.88 168.254 320 170 320 170Z" fill="currentColor"/>
|
||||
</svg>
|
||||
<h1>Astroid</h1>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div />
|
||||
|
||||
<div>
|
||||
<ThemeToggle:idle />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="layout">
|
||||
<aside class="sidebar" id="sidebar-nav">
|
||||
<SiteSidebar />
|
||||
</aside>
|
||||
<div id="article">
|
||||
<article class="content">
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
<ArticleFooter />
|
||||
</article>
|
||||
</div>
|
||||
<aside class="sidebar" id="sidebar-content">
|
||||
<DocSidebar:idle headers={headers} editHref={editHref} />
|
||||
</aside>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
35
examples/doc/src/pages/example.md
Normal file
35
examples/doc/src/pages/example.md
Normal file
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
layout: ../layouts/Main.astro
|
||||
---
|
||||
|
||||
# Markdown example
|
||||
|
||||
This is a fully-featured page, written in Markdown!
|
||||
|
||||
## Section A
|
||||
|
||||
Lorem ipsum dolor sit amet, **consectetur adipiscing elit**. Sed ut tortor _suscipit_, posuere ante id, vulputate urna. Pellentesque molestie aliquam dui sagittis aliquet. Sed sed felis convallis, lacinia lorem sit amet, fermentum ex. Etiam hendrerit mauris at elementum egestas. Vivamus id gravida ante. Praesent consectetur fermentum turpis, quis blandit tortor feugiat in. Aliquam erat volutpat. In elementum purus et tristique ornare. Suspendisse sollicitudin dignissim est a ultrices. Pellentesque sed ipsum finibus, condimentum metus eget, sagittis elit. Sed id lorem justo. Vivamus in sem ac mi molestie ornare.
|
||||
|
||||
## Section B
|
||||
|
||||
Nam quam dolor, pellentesque sed odio euismod, feugiat tempus tellus. Quisque arcu velit, ultricies in faucibus sed, ultrices ac enim. Nunc eget dictum est. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ex nisi, egestas mollis ultricies ut, laoreet suscipit libero. Nam condimentum molestie turpis. Sed vestibulum sagittis congue. Maecenas tristique enim et tincidunt tempor. Curabitur ac scelerisque nulla, in malesuada libero. Praesent eu tempus odio. Pellentesque aliquam ullamcorper quam at gravida. Sed non fringilla mauris. Aenean sit amet ultrices erat. Vestibulum congue venenatis tortor, nec suscipit tortor. Aenean pellentesque mauris eget tortor tincidunt pharetra.
|
||||
|
||||
## Section C
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: ../layouts/Main.astro
|
||||
---
|
||||
|
||||
# Markdown example
|
||||
|
||||
This is a fully-featured page, written in Markdown!
|
||||
|
||||
## Section A
|
||||
|
||||
Lorem ipsum dolor sit amet, **consectetur adipiscing elit**. Sed ut tortor _suscipit_, posuere ante id, vulputate urna. Pellentesque molestie aliquam dui sagittis aliquet. Sed sed felis convallis, lacinia lorem sit amet, fermentum ex. Etiam hendrerit mauris at elementum egestas. Vivamus id gravida ante. Praesent consectetur fermentum turpis, quis blandit tortor feugiat in. Aliquam erat volutpat. In elementum purus et tristique ornare. Suspendisse sollicitudin dignissim est a ultrices. Pellentesque sed ipsum finibus, condimentum metus eget, sagittis elit. Sed id lorem justo. Vivamus in sem ac mi molestie ornare.
|
||||
|
||||
## Section B
|
||||
|
||||
Nam quam dolor, pellentesque sed odio euismod, feugiat tempus tellus. Quisque arcu velit, ultricies in faucibus sed, ultrices ac enim. Nunc eget dictum est. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ex nisi, egestas mollis ultricies ut, laoreet suscipit libero. Nam condimentum molestie turpis. Sed vestibulum sagittis congue. Maecenas tristique enim et tincidunt tempor. Curabitur ac scelerisque nulla, in malesuada libero. Praesent eu tempus odio. Pellentesque aliquam ullamcorper quam at gravida. Sed non fringilla mauris. Aenean sit amet ultrices erat. Vestibulum congue venenatis tortor, nec suscipit tortor. Aenean pellentesque mauris eget tortor tincidunt pharetra.
|
||||
```
|
14
examples/doc/src/pages/index.astro
Normal file
14
examples/doc/src/pages/index.astro
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
import Markdown from 'astro/components/Markdown.astro';
|
||||
import Layout from '../layouts/Main.astro';
|
||||
---
|
||||
|
||||
<Layout content={{ title: 'Hello world!' }}>
|
||||
<Markdown>
|
||||
# Hello world!
|
||||
|
||||
This is a starter template, have fun building your next documentation site with [Astro](https://astro.build).
|
||||
|
||||
It offers the right mix of simple-to-use [Markdown pages](/example) and rich, interactive components embedded in `.astro` files using `<Markdown>`.
|
||||
</Markdown>
|
||||
</Layout>
|
|
@ -8,7 +8,15 @@ export default function text(parser: Parser) {
|
|||
|
||||
let data = '';
|
||||
|
||||
while (parser.index < parser.template.length && !parser.match('---') && !parser.match('<') && !parser.match('{') && !parser.match('`')) {
|
||||
const shouldContinue = () => {
|
||||
// Special case 'code' content to avoid tripping up on user code
|
||||
if (parser.current().name === 'code') {
|
||||
return !parser.match('<') && !parser.match('{');
|
||||
}
|
||||
return !parser.match('---') && !parser.match('<') && !parser.match('{') && !parser.match('`');
|
||||
}
|
||||
|
||||
while (parser.index < parser.template.length && shouldContinue()) {
|
||||
data += parser.template[parser.index++];
|
||||
}
|
||||
|
||||
|
|
|
@ -10,12 +10,12 @@ const languageMap = new Map([
|
|||
['ts', 'typescript']
|
||||
]);
|
||||
|
||||
if(lang == null) {
|
||||
if (lang == null) {
|
||||
console.warn('Prism.astro: No language provided.');
|
||||
}
|
||||
|
||||
const ensureLoaded = lang => {
|
||||
if(!Prism.languages[lang]) {
|
||||
if(lang && !Prism.languages[lang]) {
|
||||
loadLanguages([lang]);
|
||||
}
|
||||
};
|
||||
|
@ -30,14 +30,17 @@ if(languageMap.has(lang)) {
|
|||
ensureLoaded(lang);
|
||||
}
|
||||
|
||||
if(!Prism.languages[lang]) {
|
||||
if(lang && !Prism.languages[lang]) {
|
||||
console.warn(`Unable to load the language: ${lang}`);
|
||||
}
|
||||
|
||||
const grammar = Prism.languages[lang];
|
||||
let html = Prism.highlight(code, grammar, lang);
|
||||
let html = code;
|
||||
if (grammar) {
|
||||
html = Prism.highlight(code, grammar, lang);
|
||||
}
|
||||
|
||||
let className = `language-${lang}`;
|
||||
let className = lang ? `language-${lang}` : '';
|
||||
---
|
||||
|
||||
<pre class={className}><code class={className}>{html}</code></pre>
|
||||
<pre class={className}><code class={className} />{html}</pre>
|
||||
|
|
|
@ -8,12 +8,20 @@ export interface AstroConfigRaw {
|
|||
|
||||
export type ValidExtensionPlugins = 'astro' | 'react' | 'preact' | 'svelte' | 'vue';
|
||||
|
||||
export interface AstroMarkdownOptions {
|
||||
/** Enable or disable footnotes syntax extension */
|
||||
footnotes: boolean;
|
||||
/** Enable or disable GitHub-flavored Markdown syntax extension */
|
||||
gfm: boolean;
|
||||
}
|
||||
export interface AstroConfig {
|
||||
dist: string;
|
||||
projectRoot: URL;
|
||||
astroRoot: URL;
|
||||
public: URL;
|
||||
extensions?: Record<string, ValidExtensionPlugins>;
|
||||
/** 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. */
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import type { Ast, Script, Style, TemplateNode } from 'astro-parser';
|
||||
import type { CompileOptions } from '../../@types/compiler';
|
||||
import type { AstroConfig, TransformResult, ValidExtensionPlugins } from '../../@types/astro';
|
||||
import type { AstroConfig, AstroMarkdownOptions, TransformResult, ValidExtensionPlugins } from '../../@types/astro';
|
||||
|
||||
import 'source-map-support/register.js';
|
||||
import eslexer from 'es-module-lexer';
|
||||
import esbuild from 'esbuild';
|
||||
import path from 'path';
|
||||
import { walk } from 'estree-walker';
|
||||
import { parse } from 'astro-parser';
|
||||
import { walk, asyncWalk } from 'estree-walker';
|
||||
import _babelGenerator from '@babel/generator';
|
||||
import babelParser from '@babel/parser';
|
||||
import { codeFrameColumns } from '@babel/code-frame';
|
||||
|
@ -16,6 +17,7 @@ import { error, warn } from '../../logger.js';
|
|||
import { fetchContent } from './content.js';
|
||||
import { isFetchContent } from './utils.js';
|
||||
import { yellow } from 'kleur/colors';
|
||||
import { MarkdownRenderingOptions, renderMarkdown } from '../utils';
|
||||
|
||||
const traverse: typeof babelTraverse.default = (babelTraverse.default as any).default;
|
||||
|
||||
|
@ -306,7 +308,7 @@ interface CodegenState {
|
|||
components: Components;
|
||||
css: string[];
|
||||
markers: {
|
||||
insideMarkdown: boolean | string;
|
||||
insideMarkdown: boolean | Record<string, any>;
|
||||
};
|
||||
importExportStatements: Set<string>;
|
||||
dynamicImports: DynamicImportMap;
|
||||
|
@ -538,160 +540,213 @@ function compileCss(style: Style, state: CodegenState) {
|
|||
});
|
||||
}
|
||||
|
||||
/** dedent markdown */
|
||||
function dedent(str: string) {
|
||||
let arr = str.match(/^[ \t]*(?=\S)/gm);
|
||||
let first = !!arr && arr.find((x) => x.length > 0)?.length;
|
||||
return !arr || !first ? str : str.replace(new RegExp(`^[ \\t]{0,${first}}`, 'gm'), '');
|
||||
}
|
||||
|
||||
|
||||
/** Compile page markup */
|
||||
function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOptions: CompileOptions) {
|
||||
const { components, css, importExportStatements, dynamicImports, filename } = state;
|
||||
const { astroConfig } = compileOptions;
|
||||
async function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOptions: CompileOptions): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const { components, css, importExportStatements, dynamicImports, filename } = state;
|
||||
const { astroConfig } = compileOptions;
|
||||
|
||||
let outSource = '';
|
||||
walk(enterNode, {
|
||||
enter(node: TemplateNode, parent: TemplateNode) {
|
||||
switch (node.type) {
|
||||
case 'Expression': {
|
||||
let children: string[] = [];
|
||||
for (const child of node.children || []) {
|
||||
children.push(compileHtml(child, state, compileOptions));
|
||||
}
|
||||
let raw = '';
|
||||
let nextChildIndex = 0;
|
||||
for (const chunk of node.codeChunks) {
|
||||
raw += chunk;
|
||||
if (nextChildIndex < children.length) {
|
||||
raw += children[nextChildIndex++];
|
||||
}
|
||||
}
|
||||
// TODO Do we need to compile this now, or should we compile the entire module at the end?
|
||||
let code = compileExpressionSafe(raw).trim().replace(/\;$/, '');
|
||||
outSource += `,(${code})`;
|
||||
this.skip();
|
||||
break;
|
||||
}
|
||||
case 'MustacheTag':
|
||||
case 'Comment':
|
||||
return;
|
||||
case 'Fragment':
|
||||
break;
|
||||
case 'Slot':
|
||||
case 'Head':
|
||||
case 'InlineComponent':
|
||||
case 'Title':
|
||||
case 'Element': {
|
||||
const name: string = node.name;
|
||||
if (!name) {
|
||||
throw new Error('AHHHH');
|
||||
}
|
||||
try {
|
||||
const attributes = getAttributes(node.attributes);
|
||||
let paren = -1;
|
||||
let buffers = {
|
||||
out: '',
|
||||
markdown: '',
|
||||
};
|
||||
let curr: keyof typeof buffers = 'out';
|
||||
|
||||
outSource += outSource === '' ? '' : ',';
|
||||
if (node.type === 'Slot') {
|
||||
outSource += `(children`;
|
||||
return;
|
||||
/** renders markdown stored in `buffers.markdown` to JSX and pushes that to `buffers.out` */
|
||||
async function pushMarkdownToBuffer() {
|
||||
const md = buffers.markdown;
|
||||
const { markdownOptions = {} } = astroConfig;
|
||||
const { $scope: scopedClassName } = state.markers.insideMarkdown as Record<'$scope', any>;
|
||||
let { content: rendered } = await renderMarkdown(dedent(md), { ...markdownOptions as AstroMarkdownOptions, mode: 'astro-md', $: { scopedClassName: scopedClassName.slice(1, -1) } });
|
||||
const ast = parse(rendered);
|
||||
const result = await compileHtml(ast.html, {...state, markers: {...state.markers, insideMarkdown: false }}, compileOptions);
|
||||
|
||||
buffers.out += ',' + result;
|
||||
buffers.markdown = '';
|
||||
curr = 'out';
|
||||
}
|
||||
|
||||
asyncWalk(enterNode, {
|
||||
async enter(node: TemplateNode, parent: TemplateNode) {
|
||||
switch (node.type) {
|
||||
case 'Expression': {
|
||||
let children: string[] = [];
|
||||
for (const child of node.children || []) {
|
||||
children.push(await compileHtml(child, state, compileOptions));
|
||||
}
|
||||
const COMPONENT_NAME_SCANNER = /^[A-Z]/;
|
||||
if (!COMPONENT_NAME_SCANNER.test(name)) {
|
||||
outSource += `h("${name}", ${attributes ? generateAttributes(attributes) : 'null'}`;
|
||||
if (state.markers.insideMarkdown) {
|
||||
outSource += `,h(__astroMarkdownRender, null`;
|
||||
let raw = '';
|
||||
let nextChildIndex = 0;
|
||||
for (const chunk of node.codeChunks) {
|
||||
raw += chunk;
|
||||
if (nextChildIndex < children.length) {
|
||||
raw += children[nextChildIndex++];
|
||||
}
|
||||
return;
|
||||
}
|
||||
const [componentName, componentKind] = name.split(':');
|
||||
const componentImportData = components[componentName];
|
||||
if (!componentImportData) {
|
||||
throw new Error(`Unknown Component: ${componentName}`);
|
||||
// TODO Do we need to compile this now, or should we compile the entire module at the end?
|
||||
let code = compileExpressionSafe(raw).trim().replace(/\;$/, '');
|
||||
if (state.markers.insideMarkdown) {
|
||||
buffers[curr] += `{${code}}`;
|
||||
} else {
|
||||
buffers[curr] += `,(${code})`;
|
||||
}
|
||||
if (componentImportData.type === '.astro') {
|
||||
if (componentName === 'Markdown') {
|
||||
const attributeStr = attributes ? generateAttributes(attributes) : 'null';
|
||||
state.markers.insideMarkdown = attributeStr;
|
||||
outSource += `h(__astroMarkdownRender, ${attributeStr}`;
|
||||
this.skip();
|
||||
break;
|
||||
}
|
||||
case 'MustacheTag':
|
||||
case 'Comment':
|
||||
return;
|
||||
case 'Fragment':
|
||||
break;
|
||||
case 'Slot':
|
||||
case 'Head':
|
||||
case 'InlineComponent':
|
||||
case 'Title':
|
||||
case 'Element': {
|
||||
const name: string = node.name;
|
||||
if (!name) {
|
||||
throw new Error('AHHHH');
|
||||
}
|
||||
try {
|
||||
const attributes = getAttributes(node.attributes);
|
||||
|
||||
buffers.out += buffers.out === '' ? '' : ',';
|
||||
|
||||
if (node.type === 'Slot') {
|
||||
buffers[curr] += `(children`;
|
||||
paren++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], { astroConfig, dynamicImports, filename });
|
||||
if (wrapperImport) {
|
||||
importExportStatements.add(wrapperImport);
|
||||
}
|
||||
const COMPONENT_NAME_SCANNER = /^[A-Z]/;
|
||||
if (!COMPONENT_NAME_SCANNER.test(name)) {
|
||||
if (curr === 'markdown') {
|
||||
await pushMarkdownToBuffer();
|
||||
}
|
||||
buffers[curr] += `h("${name}", ${attributes ? generateAttributes(attributes) : 'null'}`;
|
||||
paren++;
|
||||
return;
|
||||
}
|
||||
const [componentName, componentKind] = name.split(':');
|
||||
const componentImportData = components[componentName];
|
||||
if (!componentImportData) {
|
||||
throw new Error(`Unknown Component: ${componentName}`);
|
||||
}
|
||||
if (componentImportData.type === '.astro') {
|
||||
if (componentName === 'Markdown') {
|
||||
const { $scope } = attributes ?? {};
|
||||
state.markers.insideMarkdown = { $scope };
|
||||
curr = 'markdown';
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], { astroConfig, dynamicImports, filename });
|
||||
if (wrapperImport) {
|
||||
importExportStatements.add(wrapperImport);
|
||||
}
|
||||
if (curr === 'markdown') {
|
||||
await pushMarkdownToBuffer();
|
||||
}
|
||||
|
||||
outSource += `h(${wrapper}, ${attributes ? generateAttributes(attributes) : 'null'}`;
|
||||
if (state.markers.insideMarkdown) {
|
||||
const attributeStr = state.markers.insideMarkdown;
|
||||
outSource += `,h(__astroMarkdownRender, ${attributeStr}`;
|
||||
paren++;
|
||||
buffers[curr] += `h(${wrapper}, ${attributes ? generateAttributes(attributes) : 'null'}`;
|
||||
} catch (err) {
|
||||
// handle errors in scope with filename
|
||||
const rel = filename.replace(astroConfig.projectRoot.pathname, '');
|
||||
// TODO: return actual codeframe here
|
||||
error(compileOptions.logging, rel, err.toString());
|
||||
}
|
||||
} catch (err) {
|
||||
// handle errors in scope with filename
|
||||
const rel = filename.replace(astroConfig.projectRoot.pathname, '');
|
||||
// TODO: return actual codeframe here
|
||||
error(compileOptions.logging, rel, err.toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'Attribute': {
|
||||
this.skip();
|
||||
return;
|
||||
}
|
||||
case 'Style': {
|
||||
css.push(node.content.styles); // if multiple <style> tags, combine together
|
||||
this.skip();
|
||||
return;
|
||||
}
|
||||
case 'CodeSpan':
|
||||
case 'CodeFence': {
|
||||
outSource += ',' + JSON.stringify(node.raw);
|
||||
return;
|
||||
}
|
||||
case 'Text': {
|
||||
const text = getTextFromAttribute(node);
|
||||
// Whitespace is significant if we are immediately inside of <Markdown>,
|
||||
// but not if we're inside of another component in <Markdown>
|
||||
if (parent.name !== 'Markdown' && !text.trim()) {
|
||||
return;
|
||||
}
|
||||
outSource += ',' + JSON.stringify(text);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new Error('Unexpected (enter) node type: ' + node.type);
|
||||
}
|
||||
},
|
||||
leave(node, parent, prop, index) {
|
||||
switch (node.type) {
|
||||
case 'Text':
|
||||
case 'CodeSpan':
|
||||
case 'CodeFence':
|
||||
case 'Attribute':
|
||||
case 'Comment':
|
||||
case 'Fragment':
|
||||
case 'Expression':
|
||||
case 'MustacheTag':
|
||||
return;
|
||||
case 'Slot':
|
||||
case 'Head':
|
||||
case 'Body':
|
||||
case 'Title':
|
||||
case 'Element':
|
||||
case 'InlineComponent': {
|
||||
if (node.type === 'InlineComponent' && node.name === 'Markdown') {
|
||||
state.markers.insideMarkdown = false;
|
||||
case 'Attribute': {
|
||||
this.skip();
|
||||
return;
|
||||
}
|
||||
if (state.markers.insideMarkdown) {
|
||||
outSource += ')';
|
||||
case 'Style': {
|
||||
css.push(node.content.styles); // if multiple <style> tags, combine together
|
||||
this.skip();
|
||||
return;
|
||||
}
|
||||
outSource += ')';
|
||||
return;
|
||||
case 'CodeSpan':
|
||||
case 'CodeFence': {
|
||||
if (state.markers.insideMarkdown) {
|
||||
if (curr === 'out') curr = 'markdown';
|
||||
buffers[curr] += node.raw;
|
||||
return;
|
||||
}
|
||||
buffers[curr] += ',' + JSON.stringify(node.data);
|
||||
return;
|
||||
}
|
||||
case 'Text': {
|
||||
let text = getTextFromAttribute(node);
|
||||
if (state.markers.insideMarkdown) {
|
||||
if (curr === 'out') curr = 'markdown';
|
||||
buffers[curr] += text;
|
||||
return;
|
||||
}
|
||||
if (parent.name !== 'Markdown' && !text.trim()) {
|
||||
return;
|
||||
}
|
||||
if (parent.name === 'code') {
|
||||
// Special case, escaped { characters from markdown content
|
||||
text = node.raw.replace(/&#123;/g, '{');
|
||||
}
|
||||
buffers[curr] += ',' + JSON.stringify(text);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new Error('Unexpected (enter) node type: ' + node.type);
|
||||
}
|
||||
case 'Style': {
|
||||
this.remove(); // this will be optimized in a global CSS file; remove so it‘s not accidentally inlined
|
||||
return;
|
||||
},
|
||||
async leave(node, parent, prop, index) {
|
||||
switch (node.type) {
|
||||
case 'Text':
|
||||
case 'Attribute':
|
||||
case 'Comment':
|
||||
case 'Fragment':
|
||||
case 'Expression':
|
||||
case 'MustacheTag':
|
||||
return;
|
||||
case 'CodeSpan':
|
||||
case 'CodeFence':
|
||||
return;
|
||||
case 'Slot':
|
||||
case 'Head':
|
||||
case 'Body':
|
||||
case 'Title':
|
||||
case 'Element':
|
||||
case 'InlineComponent': {
|
||||
if (node.type === 'InlineComponent' && curr === 'markdown' && buffers.markdown !== '') {
|
||||
await pushMarkdownToBuffer();
|
||||
}
|
||||
if (paren !== -1) {
|
||||
buffers.out += ')';
|
||||
paren--;
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'Style': {
|
||||
this.remove(); // this will be optimized in a global CSS file; remove so it‘s not accidentally inlined
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new Error('Unexpected (leave) node type: ' + node.type);
|
||||
}
|
||||
default:
|
||||
throw new Error('Unexpected (leave) node type: ' + node.type);
|
||||
}
|
||||
},
|
||||
},
|
||||
}).then(() => {
|
||||
const content = buffers.out.replace(/^\,/, '').replace(/\,\)/g, ')').replace(/\,+/g, ',').replace(/\)h/g, '),h');
|
||||
buffers.out = '';
|
||||
buffers.markdown = '';
|
||||
return resolve(content);
|
||||
});
|
||||
});
|
||||
|
||||
return outSource;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -721,7 +776,7 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
|
|||
|
||||
compileCss(ast.css, state);
|
||||
|
||||
const html = compileHtml(ast.html, state, compileOptions);
|
||||
const html = await compileHtml(ast.html, state, compileOptions);
|
||||
|
||||
return {
|
||||
script: script,
|
||||
|
|
|
@ -29,7 +29,7 @@ interface ConvertAstroOptions {
|
|||
* 2. Transform
|
||||
* 3. Codegen
|
||||
*/
|
||||
async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): Promise<TransformResult> {
|
||||
export async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): Promise<TransformResult> {
|
||||
const { filename } = opts;
|
||||
|
||||
// 1. Parse
|
||||
|
@ -48,11 +48,12 @@ async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): P
|
|||
* .md -> .astro source
|
||||
*/
|
||||
export async function convertMdToAstroSource(contents: string, { filename }: { filename: string }): Promise<string> {
|
||||
const {
|
||||
let {
|
||||
content,
|
||||
frontmatter: { layout, ...frontmatter },
|
||||
...data
|
||||
} = await renderMarkdownWithFrontmatter(contents);
|
||||
|
||||
if (frontmatter['astro'] !== undefined) {
|
||||
throw new Error(`"astro" is a reserved word but was used as a frontmatter value!\n\tat ${filename}`);
|
||||
}
|
||||
|
@ -109,7 +110,6 @@ export async function compileComponent(
|
|||
): Promise<CompileResult> {
|
||||
const result = await transformFromSource(source, { compileOptions, filename, projectRoot });
|
||||
const site = compileOptions.astroConfig.buildOptions.site || `http://localhost:${compileOptions.astroConfig.devOptions.port}`;
|
||||
const usesMarkdown = !!result.imports.find((spec) => spec.indexOf('Markdown') > -1);
|
||||
|
||||
// return template
|
||||
let modJsx = `
|
||||
|
@ -120,7 +120,6 @@ ${result.imports.join('\n')}
|
|||
|
||||
// \`__render()\`: Render the contents of the Astro module.
|
||||
import { h, Fragment } from '${internalImport('h.js')}';
|
||||
${usesMarkdown ? `import __astroMarkdownRender from '${internalImport('markdown.js')}';` : ''};
|
||||
const __astroRequestSymbol = Symbol('astro.request');
|
||||
async function __render(props, ...children) {
|
||||
const Astro = {
|
||||
|
|
41
packages/astro/src/compiler/markdown/codeblock.ts
Normal file
41
packages/astro/src/compiler/markdown/codeblock.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { visit } from 'unist-util-visit';
|
||||
|
||||
/** */
|
||||
export function remarkCodeBlock() {
|
||||
const visitor = (node: any) => {
|
||||
const { data, lang, meta } = node;
|
||||
let currentClassName = data?.hProperties?.class ?? '';
|
||||
node.data = node.data || {};
|
||||
node.data.hProperties = node.data.hProperties || {};
|
||||
node.data.hProperties = { ...node.data.hProperties, class: `language-${lang} ${currentClassName}`.trim(), lang, meta }
|
||||
|
||||
return node;
|
||||
};
|
||||
return () => (tree: any) => visit(tree, 'code', visitor);
|
||||
}
|
||||
|
||||
/** */
|
||||
export function rehypeCodeBlock() {
|
||||
const escapeCode = (code: any) => {
|
||||
code.children = code.children.map((child: any) => {
|
||||
if (child.type === 'text') {
|
||||
return { ...child, value: child.value.replace(/\{/g, '{') };
|
||||
}
|
||||
return child;
|
||||
})
|
||||
}
|
||||
const visitor = (node: any) => {
|
||||
if (node.tagName === 'code') {
|
||||
escapeCode(node);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.tagName !== 'pre') return;
|
||||
const code = node.children[0];
|
||||
if (code.tagName !== 'code') return;
|
||||
node.properties = { ...code.properties };
|
||||
|
||||
return node;
|
||||
};
|
||||
return () => (tree: any) => visit(tree, 'element', visitor);
|
||||
}
|
|
@ -2,6 +2,9 @@ declare module '@silvenon/remark-smartypants' {
|
|||
export default function (): any;
|
||||
}
|
||||
|
||||
declare module 'mdast-util-mdx';
|
||||
declare module 'micromark-extension-mdxjs';
|
||||
|
||||
declare module 'mdast-util-mdx/from-markdown.js' {
|
||||
export default function (): any;
|
||||
}
|
||||
|
|
|
@ -1,26 +1,31 @@
|
|||
import fromMarkdown from 'mdast-util-mdx/from-markdown.js';
|
||||
import toMarkdown from 'mdast-util-mdx/to-markdown.js';
|
||||
|
||||
/** See https://github.com/micromark/micromark-extension-mdx-md */
|
||||
const syntax = { disable: { null: ['autolink', 'codeIndented'] } };
|
||||
import syntaxMdxjs from 'micromark-extension-mdxjs'
|
||||
import {fromMarkdown, toMarkdown} from 'mdast-util-mdx'
|
||||
|
||||
/**
|
||||
* Lite version of https://github.com/mdx-js/mdx/tree/main/packages/remark-mdx
|
||||
* We don't need all the features MDX does because all components are precompiled
|
||||
* to HTML. We just want to disable a few MD features that cause issues.
|
||||
* Add the micromark and mdast extensions for MDX.js (JS aware MDX).
|
||||
*
|
||||
* @this {Processor}
|
||||
* @param {MdxOptions} [options]
|
||||
* @return {void}
|
||||
*/
|
||||
function mdxLite(this: any) {
|
||||
let data = this.data();
|
||||
export function remarkMdx(this: any, options: any) {
|
||||
let data = this.data()
|
||||
|
||||
add('micromarkExtensions', syntax);
|
||||
add('fromMarkdownExtensions', fromMarkdown);
|
||||
add('toMarkdownExtensions', toMarkdown);
|
||||
add('micromarkExtensions', syntaxMdxjs(options))
|
||||
add('fromMarkdownExtensions', fromMarkdown)
|
||||
add('toMarkdownExtensions', toMarkdown)
|
||||
|
||||
/** Adds remark plugin */
|
||||
/**
|
||||
* @param {string} field
|
||||
* @param {unknown} value
|
||||
*/
|
||||
function add(field: string, value: any) {
|
||||
if (data[field]) data[field].push(value);
|
||||
else data[field] = [value];
|
||||
// Other extensions defined before this.
|
||||
// Useful when externalizing.
|
||||
/* c8 ignore next 2 */
|
||||
// @ts-ignore Assume it’s an array.
|
||||
if (data[field]) data[field].push(value)
|
||||
else data[field] = [value]
|
||||
}
|
||||
}
|
||||
|
||||
export default mdxLite;
|
||||
|
|
|
@ -7,10 +7,10 @@ export default function scopedStyles(className: string) {
|
|||
if (noVisit.has(node.type)) return;
|
||||
|
||||
const { data } = node;
|
||||
const currentClassName = data?.hProperties?.class ?? '';
|
||||
let currentClassName = data?.hProperties?.class ?? '';
|
||||
node.data = node.data || {};
|
||||
node.data.hProperties = node.data.hProperties || {};
|
||||
node.data.hProperties.className = `${className} ${currentClassName}`.trim();
|
||||
node.data.hProperties.class = `${className} ${currentClassName}`.trim();
|
||||
|
||||
return node;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Transformer } from '../../@types/transformer';
|
||||
import type { Script } from 'astro-parser';
|
||||
import type { Script, TemplateNode } from 'astro-parser';
|
||||
import { getAttrValue } from '../../ast.js';
|
||||
|
||||
const PRISM_IMPORT = `import Prism from 'astro/components/Prism.astro';\n`;
|
||||
|
@ -8,7 +8,17 @@ const prismImportExp = /import Prism from ['"]astro\/components\/Prism.astro['"]
|
|||
function escape(code: string) {
|
||||
return code.replace(/[`$]/g, (match) => {
|
||||
return '\\' + match;
|
||||
});
|
||||
}).replace(/{/g, '{');
|
||||
}
|
||||
|
||||
/** Unescape { characters transformed by Markdown generation */
|
||||
function unescapeCode(code: TemplateNode) {
|
||||
code.children = code.children?.map(child => {
|
||||
if (child.type === 'Text') {
|
||||
return { ...child, raw: child.raw.replace(/&#123;/g, '{') }
|
||||
}
|
||||
return child;
|
||||
})
|
||||
}
|
||||
/** default export - Transform prism */
|
||||
export default function (module: Script): Transformer {
|
||||
|
@ -19,6 +29,11 @@ export default function (module: Script): Transformer {
|
|||
html: {
|
||||
Element: {
|
||||
enter(node) {
|
||||
if (node.name === 'code') {
|
||||
unescapeCode(node);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.name !== 'pre') return;
|
||||
const codeEl = node.children && node.children[0];
|
||||
if (!codeEl || codeEl.name !== 'code') return;
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import mdxLite from './markdown/remark-mdx-lite.js';
|
||||
import type { AstroMarkdownOptions } from '../@types/astro';
|
||||
import createCollectHeaders from './markdown/rehype-collect-headers.js';
|
||||
import scopedStyles from './markdown/remark-scoped-styles.js';
|
||||
import { remarkCodeBlock, rehypeCodeBlock } from './markdown/codeblock.js';
|
||||
import raw from 'rehype-raw';
|
||||
|
||||
import unified from 'unified';
|
||||
import markdown from 'remark-parse';
|
||||
import markdownToHtml from 'remark-rehype';
|
||||
import smartypants from '@silvenon/remark-smartypants';
|
||||
import stringify from 'rehype-stringify';
|
||||
// import smartypants from '@silvenon/remark-smartypants';
|
||||
import rehypeStringify from 'rehype-stringify';
|
||||
|
||||
export interface MarkdownRenderingOptions {
|
||||
export interface MarkdownRenderingOptions extends Partial<AstroMarkdownOptions> {
|
||||
$?: {
|
||||
scopedClassName: string | null;
|
||||
};
|
||||
footnotes?: boolean;
|
||||
gfm?: boolean;
|
||||
plugins?: any[];
|
||||
mode: 'md'|'astro-md';
|
||||
}
|
||||
|
||||
/** Internal utility for rendering a full markdown file and extracting Frontmatter data */
|
||||
|
@ -22,16 +22,16 @@ export async function renderMarkdownWithFrontmatter(contents: string, opts?: Mar
|
|||
// Dynamic import to ensure that "gray-matter" isn't built by Snowpack
|
||||
const { default: matter } = await import('gray-matter');
|
||||
const { data: frontmatter, content } = matter(contents);
|
||||
const value = await renderMarkdown(content, opts);
|
||||
const value = await renderMarkdown(content, { ...opts, mode: 'md' });
|
||||
return { ...value, frontmatter };
|
||||
}
|
||||
|
||||
/** Shared utility for rendering markdown */
|
||||
export async function renderMarkdown(content: string, opts?: MarkdownRenderingOptions | null) {
|
||||
const { $: { scopedClassName = null } = {}, footnotes: useFootnotes = true, gfm: useGfm = true, plugins = [] } = opts ?? {};
|
||||
const { $: { scopedClassName = null } = {}, mode = 'astro-md', footnotes: useFootnotes = true, gfm: useGfm = true } = opts ?? {};
|
||||
const { headers, rehypeCollectHeaders } = createCollectHeaders();
|
||||
|
||||
let parser = unified().use(markdown).use(mdxLite).use(smartypants);
|
||||
let parser = unified().use(markdown).use(remarkCodeBlock());
|
||||
|
||||
if (scopedClassName) {
|
||||
parser = parser.use(scopedStyles(scopedClassName));
|
||||
|
@ -53,7 +53,8 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
|
|||
.use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw'] })
|
||||
.use(raw)
|
||||
.use(rehypeCollectHeaders)
|
||||
.use(stringify)
|
||||
.use(rehypeCodeBlock())
|
||||
.use(rehypeStringify)
|
||||
.process(content);
|
||||
result = vfile.contents.toString();
|
||||
} catch (err) {
|
||||
|
|
|
@ -54,6 +54,7 @@ function configDefaults(userConfig?: any): any {
|
|||
if (!config.devOptions) config.devOptions = {};
|
||||
if (!config.devOptions.port) config.devOptions.port = 3000;
|
||||
if (!config.buildOptions) config.buildOptions = {};
|
||||
if (!config.markdownOptions) config.markdownOptions = {};
|
||||
if (typeof config.buildOptions.sitemap === 'undefined') config.buildOptions.sitemap = true;
|
||||
|
||||
return config;
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import { renderMarkdown } from '../compiler/utils.js';
|
||||
|
||||
/**
|
||||
* Functional component which uses Astro's built-in Markdown rendering
|
||||
* to render out its children.
|
||||
*
|
||||
* Note: the children have already been properly escaped/rendered
|
||||
* by the parser and Astro, so at this point we're just rendering
|
||||
* out plain markdown, no need for JSX support
|
||||
*/
|
||||
export default async function Markdown(props: { $scope: string | null }, ...children: string[]): Promise<string> {
|
||||
const { $scope = null } = props ?? {};
|
||||
const text = dedent(children.join('').trimEnd());
|
||||
let { content } = await renderMarkdown(text, { $: { scopedClassName: $scope } });
|
||||
if (content.split('<p>').length === 2) {
|
||||
content = content.replace(/^\<p\>/i, '').replace(/\<\/p\>$/i, '');
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
/** Remove leading indentation based on first line */
|
||||
function dedent(str: string) {
|
||||
let arr = str.match(/^[ \t]*(?=\S)/gm);
|
||||
let first = !!arr && arr.find((x) => x.length > 0)?.length;
|
||||
return !arr || !first ? str : str.replace(new RegExp(`^[ \\t]{0,${first}}`, 'gm'), '');
|
||||
}
|
|
@ -314,7 +314,12 @@ async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackO
|
|||
},
|
||||
packageOptions: {
|
||||
knownEntrypoints: ['preact-render-to-string'],
|
||||
external: ['@vue/server-renderer', 'node-fetch', 'prismjs/components/index.js'],
|
||||
external: [
|
||||
'@vue/server-renderer',
|
||||
'node-fetch',
|
||||
'prismjs/components/index.js',
|
||||
'gray-matter',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -2218,6 +2218,14 @@ astral-regex@^2.0.0:
|
|||
|
||||
"astro-languageserver@file:tools/astro-languageserver":
|
||||
version "0.4.0"
|
||||
dependencies:
|
||||
source-map "^0.7.3"
|
||||
typescript "^4.3.1-rc"
|
||||
vscode-css-languageservice "^5.1.1"
|
||||
vscode-emmet-helper "2.1.2"
|
||||
vscode-html-languageservice "^3.0.3"
|
||||
vscode-languageserver "6.1.1"
|
||||
vscode-languageserver-textdocument "^1.0.1"
|
||||
|
||||
"astro-scripts@file:scripts":
|
||||
version "0.0.1"
|
||||
|
|
Loading…
Reference in a new issue