mirror of https://github.com/sveltejs/svelte
chore: remove svelte.dev (#13794)
parent
be131575d8
commit
03c4a7da53
File diff suppressed because it is too large
Load Diff
@ -1,17 +0,0 @@
|
||||
# for local development, copy this file to .env.local file and
|
||||
# fill in the blanks
|
||||
|
||||
# server-side
|
||||
GITHUB_CLIENT_ID=
|
||||
GITHUB_CLIENT_SECRET=
|
||||
|
||||
# Path to local copy of Svelte relative from sites/svelte.dev. Used by the REPL.
|
||||
# Optional. The default value assumes the svelte repo and sites repo are in the same directory.
|
||||
# LOCAL_SVELTE_PATH=../../../svelte
|
||||
|
||||
# staging database
|
||||
SUPABASE_URL=https://kpaaohfbmxvespqoqdzp.supabase.co
|
||||
SUPABASE_KEY=
|
||||
|
||||
# client-side
|
||||
VITE_MAPBOX_ACCESS_TOKEN=
|
@ -1,13 +0,0 @@
|
||||
/.env.local
|
||||
/.svelte-kit/
|
||||
/build/
|
||||
/functions/
|
||||
|
||||
/static/svelte-app.json
|
||||
/scripts/svelte-app/
|
||||
/src/routes/_components/Supporters/contributors.jpg
|
||||
/src/routes/_components/Supporters/contributors.js
|
||||
/src/routes/_components/Supporters/donors.jpg
|
||||
/src/routes/_components/Supporters/donors.js
|
||||
/src/lib/generated
|
||||
.vercel
|
@ -1,212 +0,0 @@
|
||||
## Running locally
|
||||
|
||||
A local database is only required in dev mode if you want to test reading and writing saved REPLs on it. Without a local database in dev mode, the REPL will be able to load saved REPLs from the production database, but not save them.
|
||||
|
||||
Note also that in dev mode, the REPL requires support for [`import` statements in web workers](https://caniuse.com/mdn-javascript_operators_import_worker_support), [as noted in the Vite documentation](https://vitejs.dev/guide/features.html#web-workers). You may need to update your browser to the latest version.
|
||||
|
||||
If you do want to use a database, set it up on [Supabase](https://supabase.com) with the instructions [here](https://github.com/sveltejs/sites/tree/master/db) and set the corresponding environment variables.
|
||||
|
||||
Build the `svelte` package, then run the site sub-project:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm --dir ../../packages/svelte build
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
and navigate to [localhost:5173](http://localhost:5173).
|
||||
|
||||
The first time you run the site locally, it will update the list of Contributors, REPL dependencies and examples data that is used on the [examples page](https://svelte-dev-2.vercel.app/examples). After this it won't run again unless you force it by running:
|
||||
|
||||
```bash
|
||||
pnpm run update
|
||||
```
|
||||
|
||||
## Running using the local copy of Svelte
|
||||
|
||||
By default, the REPL will fetch the most recent version of Svelte from https://unpkg.com/svelte. When running the site locally, you can also use your local copy of Svelte.
|
||||
|
||||
To produce the proper browser-compatible UMD build of the compiler, you will need to run `npm run build` (or `npm run dev`) in the `svelte` repository with the `PUBLISH` environment variable set to any non-empty string:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/sveltejs/svelte.git
|
||||
cd svelte
|
||||
pnpm i --frozen-lockfile
|
||||
PUBLISH=1 npm run build
|
||||
```
|
||||
|
||||
The default configuration assumes that the `sites` repository and the `svelte` repository are in the same directory. If not, you can set `LOCAL_SVELTE_PATH` in `sites/svelte.dev/.env` to a different path to the local copy of Svelte.
|
||||
|
||||
Then visit the REPL at [localhost:5173/repl?version=local](http://localhost:5173/repl?version=local). Please note that the local REPL only works with `pnpm dev` and not when building the site for production usage.
|
||||
|
||||
## REPL GitHub integration
|
||||
|
||||
In order for the REPL's GitHub integration to work properly when running locally, you will need to:
|
||||
|
||||
- [create a GitHub OAuth app](https://github.com/settings/developers):
|
||||
- set `Authorization callback URL` to `http://localhost:5173/auth/callback`;
|
||||
- set `Application name` as you like, and `Homepage URL` as `http://localhost:5173/`;
|
||||
- create the app and take note of `Client ID` and `Client Secret`
|
||||
- in this directory, create an `.env.local` file (see `.env.example`) containing:
|
||||
```
|
||||
GITHUB_CLIENT_ID=[your app's Client ID]
|
||||
GITHUB_CLIENT_SECRET=[your app's Client Secret]
|
||||
```
|
||||
|
||||
The GitHub app requires a specific callback URL, and so cannot be used with the preview deployment in the staging environment.
|
||||
|
||||
## Building the site
|
||||
|
||||
To build the website, run `pnpm build`. The output can be found in `.vercel`.
|
||||
|
||||
## Testing
|
||||
|
||||
Tests can be run using `pnpm test`.
|
||||
|
||||
## Docs & other content
|
||||
|
||||
All the docs, examples, tutorials, FAQ live in the [documentation](../../documentation) directory, outside the site sub-project. If you modify these, and your app server is running, you will need to reload the page to see the changes.
|
||||
|
||||
Following are the file structures of the different kind of documentations
|
||||
|
||||
### Docs structure
|
||||
|
||||
```txt
|
||||
- documentation/docs
|
||||
- 01-getting-started <- Category
|
||||
- meta.json <- Metadata
|
||||
- 01-introduction.md <- Page
|
||||
- 02-template-syntax <- Category
|
||||
- meta.json <- Metadata
|
||||
- 01-logic-blocks.md <- Page
|
||||
- 02-special-tags.md <- Page
|
||||
- 03-element-directives.md <- Page
|
||||
```
|
||||
|
||||
If you are creating a new page, it must be within a category. That is, you can't have a .md file in the `docs` directory's root level. You may have a category without any pages in it, but you can't have a page without a category. You can add the new page in an existing category, or create your own, for example:
|
||||
|
||||
```txt
|
||||
- documentation/docs
|
||||
<!-- Rest of the docs -->
|
||||
+ - 07-my-new-category <- Category
|
||||
+ - 01-my-new-page.md <- Page
|
||||
```
|
||||
|
||||
The numbers in front of category folders and page files are just for ordering the content. They may not be consecutive. Their only purpose exists for the docs author to decide how the content is arranged.
|
||||
|
||||
> Because of hardcoded regex in docs processing code, the numbers prefixed to pages are REQUIRED and _must be two digits_.
|
||||
|
||||
The name of the file is what determines the URL of the page. For example, the URL of `01-introduction.md` is `https://svelte.dev/docs/introduction`. The URL of `02-special-tags.md` is `https://svelte.dev/docs/special-tags`. Even though these are in categories, the URL does not contain the category name. Keep this in mind when creating new pages, as two pages with same slug in different categories will clash.
|
||||
|
||||
**meta.json** files contain data about the current category. At the time of writing it only has one field: `title`
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Getting Started"
|
||||
}
|
||||
```
|
||||
|
||||
This `title` field is used as category text in the sidebar on docs page.
|
||||
|
||||
Every single .md file in the docs must have frontmatter with `title` in it. For example, this is how the frontmatter of `02-logic-blocks.md` looks like:
|
||||
|
||||
```md
|
||||
---
|
||||
title: .svelte files
|
||||
---
|
||||
|
||||
Components are the building blocks of Svelte applications. They are written into `.svelte` files, using a superset of HTML.
|
||||
|
||||
All three sections — script, styles and markup — are optional.
|
||||
|
||||
<!-- REST OF THE CONTENT -->
|
||||
```
|
||||
|
||||
You need not specify a h1 tag(or in markdown, a `#`). The `title` field in the frontmatter will be used as the h1 tag.
|
||||
|
||||
The headings in the document must start from h2(`##`). That is, you can't have an h1 tag in the document. h2(`##`), h3(`###`), h4(`####`) and h5(`#####`) are all valid.
|
||||
|
||||
#### Processing
|
||||
|
||||
Docs are processed in the [`src/lib/server/docs/index.js`](./src/lib/server/docs/index.js) file. It is responsible for reading the docs from filesystem and accumulating the metadata in forms of arrays and objects and for _rendering_ the markdown files into HTML. These functions are then imported into [src/routes/docs/+layout.server.js](./src/routes/docs/+layout.server.js) and used to generate docs list, and similarly in [src/routes/docs/%5Bslug%5D/+page.server.js](./src/routes/docs/%5Bslug%5D/%2Bpage.server.js) and are rendered there.
|
||||
|
||||
### Tutorial structure
|
||||
|
||||
```txt
|
||||
- documentation/tutorial
|
||||
- 01-introduction <- Category
|
||||
- meta.json <- Metadata
|
||||
- 01-basics <- Page's content folder
|
||||
- text.md <- Text content of tutorial
|
||||
- app-a <- The initial app folder
|
||||
- App.svelte
|
||||
- store.js
|
||||
- app-b <- The final app folder. Not always present
|
||||
- App.svelte
|
||||
- store.js
|
||||
```
|
||||
|
||||
Similar to how docs are structured, only difference is that the pages are in a folders, and their content is in a `text.md` file. Alongside, are two folders, _app-a_ and _app-b_. These are the initial and final apps respectively. The initial app is the one that the tutorial shows, and the final app is the one that the tutorial switches to after user clicks on the **Show me** button.
|
||||
|
||||
> app-b is not always there. This means that the _Show me_ button is not present for that page.
|
||||
|
||||
The naming scheme of docs is followed here as well. The numbers in front of the folders are just for ordering the content. They may not be consecutive. Their only purpose exists for the tutorial author to decide how the content is arranged. _And they are compulsary_.
|
||||
|
||||
#### Processing
|
||||
|
||||
Tutorials are processed in the [`src/lib/server/tutorial/index.js`](./src/lib/server/tutorial/index.js) file. It is responsible for reading the tutorials from filesystem and accumulating the metadata in forms of arrays and objects and has the code responsible for _rendering_ the markdown files into HTML. These functions are then imported into [src/routes/tutorial/+layout.server.js](./src/routes/tutorial/%2Blayout.server.js) and used to generate tutorial list, and similarly in [src/routes/tutorial/%5Bslug%5D/+page.server.js](./src/routes/tutorial/%5Bslug%5D/%2Bpage.server.js) and are rendered there.
|
||||
|
||||
### Examples structure
|
||||
|
||||
```txt
|
||||
- documentation/examples
|
||||
- 00-introduction <- Category
|
||||
- meta.json <- Metadata
|
||||
- 00-hello-world <- Page's content folder
|
||||
- meta.json <- Metadata
|
||||
- App.svelte <- code files
|
||||
- 01-reactivity <- Category
|
||||
- meta.json <- Metadata
|
||||
- 00-reactive-assignments <- Page's content folder
|
||||
- meta.json <- Metadata
|
||||
- App.svelte <- code files
|
||||
```
|
||||
|
||||
Similar to the tutorial, only difference: There is no `text.md`, and the code files are kept right in the folder, not in `app-` folder.
|
||||
|
||||
Same naming scheme as docs and tutorial is followed.
|
||||
|
||||
#### Processing
|
||||
|
||||
Examples are processed in the [`src/lib/server/examples/index.js`](./src/lib/server/examples/index.js) folder. It is responsible for reading the examples from filesystem and accumulating the metadata in forms of arrays and objects, and for _rendering_ the markdown files into HTML. These functions are then imported into [src/routes/examples/%5Bslug%5D/+page.server.js](./src/routes/examples/%5Bslug%5D/%2Bpage.server.js) and are rendered there.
|
||||
|
||||
### Blog structure
|
||||
|
||||
```txt
|
||||
- documentation/blog
|
||||
- 2019-01-01-my-first-post.md
|
||||
- 2019-01-02-my-second-post.md
|
||||
```
|
||||
|
||||
Compared to the rest of the content, blog posts are not in a folder. They are placed at the root of `documentation/blog` folder. The name of the file is the date of the post, followed by the slug of the post. The slug is the URL where the blog post is available. For example, the slug of `2019-01-01-my-first-post.md` is `my-first-post`.
|
||||
|
||||
All the metadata about the blog post is mentioned in the frontematter of a post. For example, this is how the frontmatter of [2023-03-09-zero-config-type-safety.md](../../documentation/blog/2023-03-09-zero-config-type-safety.md) looks like:
|
||||
|
||||
```md
|
||||
---
|
||||
title: Zero-effort type safety
|
||||
description: More convenience and correctness, less boilerplate
|
||||
author: Simon Holthausen
|
||||
authorURL: https://twitter.com/dummdidumm_
|
||||
---
|
||||
```
|
||||
|
||||
#### Processing
|
||||
|
||||
Blog posts are processed in the [`src/lib/server/blog/index.js`](./src/lib/server/blog/index.js) file. It is responsible for reading the blog posts from filesystem and accumulating the metadata in forms of arrays and objects, and for _rendering_ the markdown files into HTML. These functions are then imported into [src/routes/blog/+page.svelte](./src/routes/blog/%2Bpage.server.js), where they show the list of blogs. The rendering function is imported in [src/routes/blog/%5Bslug%5D/+page.server.js](./src/routes/blog/%5Bslug%5D/%2Bpage.server.js) and renders the individual blog post there.
|
||||
|
||||
## Translating the API docs
|
||||
|
||||
Anchors are automatically generated using headings in the documentation and by default (for the english language) they are latinised to make sure the URL is always conforming to RFC3986.
|
||||
|
||||
If we need to translate the API documentation to a language using unicode chars, we can setup this app to export the correct anchors by setting up `SLUG_PRESERVE_UNICODE` to `true` in `config.js`.
|
@ -1,2 +0,0 @@
|
||||
export const SLUG_PRESERVE_UNICODE = false;
|
||||
export const SLUG_SEPARATOR = '_';
|
@ -1,56 +0,0 @@
|
||||
{
|
||||
"name": "svelte.dev",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "Docs and examples for Svelte",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node scripts/update.js && pnpm run generate && vite dev",
|
||||
"build": "node scripts/update.js && pnpm run generate && vite build",
|
||||
"generate": "node scripts/type-gen/index.js && node scripts/generate_examples.js",
|
||||
"update": "node scripts/update.js --force=true",
|
||||
"preview": "vite preview",
|
||||
"start": "node build",
|
||||
"check": "node scripts/update.js && pnpm generate && svelte-kit sync && svelte-check",
|
||||
"check:watch": "svelte-kit sync && svelte-check --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@supabase/supabase-js": "^2.39.3",
|
||||
"@sveltejs/repl": "0.6.0",
|
||||
"cookie": "^0.6.0",
|
||||
"devalue": "^4.3.2",
|
||||
"do-not-zip": "^1.0.0",
|
||||
"flexsearch": "^0.7.43",
|
||||
"flru": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@resvg/resvg-js": "^2.6.0",
|
||||
"@sveltejs/adapter-vercel": "^5.4.3",
|
||||
"@sveltejs/kit": "^2.4.3",
|
||||
"@sveltejs/site-kit": "6.0.0-next.59",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.0",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/node": "^20.11.5",
|
||||
"browserslist": "^4.22.2",
|
||||
"degit": "^2.8.4",
|
||||
"dotenv": "^16.3.2",
|
||||
"jimp": "^0.22.10",
|
||||
"lightningcss": "^1.23.0",
|
||||
"magic-string": "^0.30.11",
|
||||
"marked": "^11.1.1",
|
||||
"sass": "^1.70.0",
|
||||
"satori": "^0.10.11",
|
||||
"satori-html": "^0.3.2",
|
||||
"shelljs": "^0.8.5",
|
||||
"shiki": "^0.14.7",
|
||||
"shiki-twoslash": "^3.1.2",
|
||||
"svelte": "^4.2.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-preprocess": "^5.1.3",
|
||||
"tiny-glob": "^0.2.9",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.6",
|
||||
"vite-imagetools": "^6.2.9"
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
<p>see <a href="https://github.com/sveltejs/svelte/pull/9424">#9424</a></p>
|
@ -1,16 +0,0 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { get_examples_data } from '../src/lib/server/examples/index.js';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
|
||||
const examples_data = await get_examples_data(
|
||||
fileURLToPath(new URL('../../../documentation/examples', import.meta.url))
|
||||
);
|
||||
|
||||
try {
|
||||
await mkdir(new URL('../src/lib/generated/', import.meta.url), { recursive: true });
|
||||
} catch {}
|
||||
|
||||
writeFile(
|
||||
new URL('../src/lib/generated/examples-data.js', import.meta.url),
|
||||
`export default ${JSON.stringify(examples_data)}`
|
||||
);
|
@ -1,76 +0,0 @@
|
||||
// @ts-check
|
||||
import 'dotenv/config';
|
||||
import Jimp from 'jimp';
|
||||
import { stat, writeFile } from 'node:fs/promises';
|
||||
import { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const force = process.env.FORCE_UPDATE === 'true';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
process.chdir(__dirname);
|
||||
|
||||
// ../src/routes/_components/Supporters/contributors.js
|
||||
const outputFile = new URL(`../src/routes/_components/Supporters/contributors.js`, import.meta.url);
|
||||
|
||||
try {
|
||||
if (!force && (await stat(outputFile))) {
|
||||
console.info(`[update/contributors] ${outputFile} exists. Skipping`);
|
||||
process.exit(0);
|
||||
}
|
||||
} catch {
|
||||
const base = `https://api.github.com/repos/sveltejs/svelte/contributors`;
|
||||
const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } = process.env;
|
||||
|
||||
const MAX = 24;
|
||||
const SIZE = 128;
|
||||
|
||||
const contributors = [];
|
||||
let page = 1;
|
||||
|
||||
while (true) {
|
||||
const res = await fetch(
|
||||
`${base}?client_id=${GITHUB_CLIENT_ID}&client_secret=${GITHUB_CLIENT_SECRET}&per_page=100&page=${page++}`
|
||||
);
|
||||
const list = await res.json();
|
||||
|
||||
if (!Array.isArray(list)) throw new Error('Expected an array');
|
||||
|
||||
if (list.length === 0) break;
|
||||
|
||||
contributors.push(...list);
|
||||
}
|
||||
|
||||
const authors = contributors
|
||||
.filter(({ login }) => !login.includes('[bot]'))
|
||||
.sort((a, b) => b.contributions - a.contributions)
|
||||
.slice(0, MAX);
|
||||
|
||||
const sprite = new Jimp(SIZE * authors.length, SIZE);
|
||||
|
||||
for (let i = 0; i < authors.length; i += 1) {
|
||||
const author = authors[i];
|
||||
console.log(`${i + 1} / ${authors.length}: ${author.login}`);
|
||||
|
||||
const image_data = await fetch(author.avatar_url);
|
||||
const buffer = await image_data.arrayBuffer();
|
||||
|
||||
// @ts-ignore
|
||||
const image = await Jimp.read(buffer);
|
||||
image.resize(SIZE, SIZE);
|
||||
|
||||
sprite.composite(image, i * SIZE, 0);
|
||||
}
|
||||
|
||||
await sprite
|
||||
.quality(80)
|
||||
.writeAsync(
|
||||
fileURLToPath(
|
||||
new URL(`../src/routes/_components/Supporters/contributors.jpg`, import.meta.url)
|
||||
)
|
||||
);
|
||||
|
||||
const str = `[\n\t${authors.map((a) => `'${a.login}'`).join(',\n\t')}\n]`;
|
||||
|
||||
writeFile(outputFile, `export default ${str};`);
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
// @ts-check
|
||||
import 'dotenv/config';
|
||||
import Jimp from 'jimp';
|
||||
import { stat, writeFile } from 'node:fs/promises';
|
||||
import { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const force = process.env.FORCE_UPDATE === 'true';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
process.chdir(__dirname);
|
||||
|
||||
const outputFile = new URL(`../src/routes/_components/Supporters/donors.js`, import.meta.url);
|
||||
|
||||
try {
|
||||
if (!force && (await stat(outputFile))) {
|
||||
console.info(`[update/donors] ${outputFile} exists. Skipping`);
|
||||
process.exit(0);
|
||||
}
|
||||
} catch {
|
||||
const MAX = 24;
|
||||
const SIZE = 128;
|
||||
|
||||
const res = await fetch('https://opencollective.com/svelte/members/all.json');
|
||||
const donors = await res.json();
|
||||
|
||||
if (!Array.isArray(donors)) throw new Error('Expected an array');
|
||||
|
||||
const unique = new Map();
|
||||
donors.forEach((d) => unique.set(d.profile, d));
|
||||
|
||||
let backers = [...unique.values()]
|
||||
.filter(({ role }) => role === 'BACKER')
|
||||
.sort((a, b) => b.totalAmountDonated - a.totalAmountDonated)
|
||||
.slice(0, 3 * MAX);
|
||||
|
||||
const included = [];
|
||||
for (let i = 0; included.length < MAX; i += 1) {
|
||||
const backer = backers[i];
|
||||
console.log(`${included.length + 1} / ${MAX}: ${backer.name}`);
|
||||
|
||||
try {
|
||||
const image_data = await fetch(backer.image);
|
||||
const buffer = await image_data.arrayBuffer();
|
||||
// @ts-ignore
|
||||
const image = await Jimp.read(buffer);
|
||||
image.resize(SIZE, SIZE);
|
||||
included.push({ backer, image });
|
||||
} catch (err) {
|
||||
console.log(`Skipping ${backer.name}: no image data`);
|
||||
}
|
||||
}
|
||||
|
||||
const sprite = new Jimp(SIZE * included.length, SIZE);
|
||||
for (let i = 0; i < included.length; i += 1) {
|
||||
sprite.composite(included[i].image, i * SIZE, 0);
|
||||
}
|
||||
|
||||
await sprite
|
||||
.quality(80)
|
||||
.writeAsync(
|
||||
fileURLToPath(new URL(`../src/routes/_components/Supporters/donors.jpg`, import.meta.url))
|
||||
);
|
||||
|
||||
const str = `[\n\t${included.map((a) => `${JSON.stringify(a.backer.name)}`).join(',\n\t')}\n]`;
|
||||
|
||||
writeFile(outputFile, `export default ${str};`);
|
||||
}
|
@ -1,314 +0,0 @@
|
||||
// @ts-check
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { format } from 'prettier';
|
||||
import ts from 'typescript';
|
||||
|
||||
/** @typedef {{
|
||||
* name: string;
|
||||
* comment: string;
|
||||
* markdown?: string;
|
||||
* snippet: string;
|
||||
* deprecated: string | null;
|
||||
* children: Extracted[] }
|
||||
* } Extracted */
|
||||
|
||||
/** @type {Array<{ name: string; comment: string; exports: Extracted[]; types: Extracted[]; exempt?: boolean; }>} */
|
||||
const modules = [];
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @param {ts.NodeArray<ts.Statement>} statements
|
||||
*/
|
||||
async function get_types(code, statements) {
|
||||
/** @type {Extracted[]} */
|
||||
const exports = [];
|
||||
|
||||
/** @type {Extracted[]} */
|
||||
const types = [];
|
||||
|
||||
if (statements) {
|
||||
for (const statement of statements) {
|
||||
const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined;
|
||||
|
||||
const export_modifier = modifiers?.find(
|
||||
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword
|
||||
);
|
||||
|
||||
if (!export_modifier) continue;
|
||||
|
||||
if (
|
||||
ts.isClassDeclaration(statement) ||
|
||||
ts.isInterfaceDeclaration(statement) ||
|
||||
ts.isTypeAliasDeclaration(statement) ||
|
||||
ts.isModuleDeclaration(statement) ||
|
||||
ts.isVariableStatement(statement) ||
|
||||
ts.isFunctionDeclaration(statement)
|
||||
) {
|
||||
const name_node = ts.isVariableStatement(statement)
|
||||
? statement.declarationList.declarations[0]
|
||||
: statement;
|
||||
|
||||
// @ts-ignore no idea why it's complaining here
|
||||
const name = name_node.name?.escapedText;
|
||||
|
||||
let start = statement.pos;
|
||||
let comment = '';
|
||||
/** @type {string | null} */
|
||||
let deprecated_notice = null;
|
||||
|
||||
// @ts-ignore i think typescript is bad at typescript
|
||||
if (statement.jsDoc) {
|
||||
// @ts-ignore
|
||||
const jsDoc = statement.jsDoc[0];
|
||||
|
||||
comment = jsDoc.comment;
|
||||
|
||||
if (jsDoc?.tags?.[0]?.tagName?.escapedText === 'deprecated') {
|
||||
deprecated_notice = jsDoc.tags[0].comment;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
start = jsDoc.end;
|
||||
}
|
||||
|
||||
const i = code.indexOf('export', start);
|
||||
start = i + 6;
|
||||
|
||||
/** @type {Extracted[]} */
|
||||
let children = [];
|
||||
|
||||
let snippet_unformatted = code.slice(start, statement.end).trim();
|
||||
|
||||
if (ts.isInterfaceDeclaration(statement) || ts.isClassDeclaration(statement)) {
|
||||
if (statement.members.length > 0) {
|
||||
for (const member of statement.members) {
|
||||
// @ts-ignore
|
||||
children.push(munge_type_element(member));
|
||||
}
|
||||
|
||||
children = children.filter(Boolean);
|
||||
|
||||
// collapse `interface Foo {/* lots of stuff*/}` into `interface Foo {…}`
|
||||
const first = statement.members.at(0);
|
||||
const last = statement.members.at(-1);
|
||||
|
||||
let body_start = first.pos - start;
|
||||
while (snippet_unformatted[body_start] !== '{') body_start -= 1;
|
||||
|
||||
let body_end = last.end - start;
|
||||
while (snippet_unformatted[body_end] !== '}') body_end += 1;
|
||||
|
||||
snippet_unformatted =
|
||||
snippet_unformatted.slice(0, body_start + 1) +
|
||||
'/*…*/' +
|
||||
snippet_unformatted.slice(body_end);
|
||||
}
|
||||
}
|
||||
|
||||
const snippet = (
|
||||
await format(snippet_unformatted, {
|
||||
parser: 'typescript',
|
||||
printWidth: 60,
|
||||
useTabs: true,
|
||||
singleQuote: true,
|
||||
trailingComma: 'none'
|
||||
})
|
||||
)
|
||||
.replace(/\s*(\/\*…\*\/)\s*/g, '/*…*/')
|
||||
.trim();
|
||||
|
||||
const collection =
|
||||
ts.isVariableStatement(statement) || ts.isFunctionDeclaration(statement)
|
||||
? exports
|
||||
: types;
|
||||
|
||||
collection.push({
|
||||
name,
|
||||
comment,
|
||||
snippet,
|
||||
children,
|
||||
deprecated: deprecated_notice
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
types.sort((a, b) => (a.name < b.name ? -1 : 1));
|
||||
exports.sort((a, b) => (a.name < b.name ? -1 : 1));
|
||||
}
|
||||
|
||||
return { types, exports };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ts.TypeElement} member
|
||||
*/
|
||||
function munge_type_element(member, depth = 1) {
|
||||
// @ts-ignore
|
||||
const doc = member.jsDoc?.[0];
|
||||
|
||||
if (/(private api|do not use)/i.test(doc?.comment)) return;
|
||||
|
||||
/** @type {string[]} */
|
||||
const children = [];
|
||||
|
||||
const name = member.name?.escapedText;
|
||||
let snippet = member.getText();
|
||||
|
||||
for (let i = -1; i < depth; i += 1) {
|
||||
snippet = snippet.replace(/^\t/gm, '');
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isPropertySignature(member) &&
|
||||
ts.isTypeLiteralNode(member.type) &&
|
||||
member.type.members.some((member) => member.jsDoc?.[0].comment)
|
||||
) {
|
||||
let a = 0;
|
||||
while (snippet[a] !== '{') a += 1;
|
||||
|
||||
snippet = snippet.slice(0, a + 1) + '/*…*/}';
|
||||
|
||||
for (const child of member.type.members) {
|
||||
children.push(munge_type_element(child, depth + 1));
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const bullets = [];
|
||||
|
||||
for (const tag of doc?.tags ?? []) {
|
||||
const type = tag.tagName.escapedText;
|
||||
|
||||
switch (tag.tagName.escapedText) {
|
||||
case 'private':
|
||||
bullets.push(`- <span class="tag">private</span> ${tag.comment}`);
|
||||
break;
|
||||
|
||||
case 'readonly':
|
||||
bullets.push(`- <span class="tag">readonly</span> ${tag.comment}`);
|
||||
break;
|
||||
|
||||
case 'param':
|
||||
bullets.push(`- \`${tag.name.getText()}\` ${tag.comment}`);
|
||||
break;
|
||||
|
||||
case 'default':
|
||||
bullets.push(`- <span class="tag">default</span> \`${tag.comment}\``);
|
||||
break;
|
||||
|
||||
case 'returns':
|
||||
bullets.push(`- <span class="tag">returns</span> ${tag.comment}`);
|
||||
break;
|
||||
|
||||
case 'deprecated':
|
||||
bullets.push(`- <span class="tag deprecated">deprecated</span> ${tag.comment}`);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`unhandled JSDoc tag: ${type}`); // TODO indicate deprecated stuff
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
snippet,
|
||||
comment: (doc?.comment ?? '')
|
||||
.replace(/\/\/\/ type: (.+)/g, '/** @type {$1} */')
|
||||
.replace(/^( )+/gm, (match, spaces) => {
|
||||
return '\t'.repeat(match.length / 2);
|
||||
}),
|
||||
bullets,
|
||||
children
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Type declarations include fully qualified URLs so that they become links when
|
||||
* you hover over names in an editor with TypeScript enabled. We need to remove
|
||||
* the origin so that they become root-relative, so that they work in preview
|
||||
* deployments and when developing locally
|
||||
* @param {string} str
|
||||
*/
|
||||
function strip_origin(str) {
|
||||
return str.replace(/https:\/\/svelte\.dev/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
*/
|
||||
async function read_d_ts_file(file) {
|
||||
const resolved = path.resolve('../../packages/svelte', file);
|
||||
|
||||
// We can't use JSDoc comments inside JSDoc, so we would get ts(7031) errors if
|
||||
// we didn't ignore this error specifically for `/// file:` code examples
|
||||
const str = await readFile(resolved, 'utf-8');
|
||||
|
||||
return str.replace(/(\s*\*\s*)```js([\s\S]+?)```/g, (match, prefix, code) => {
|
||||
return `${prefix}\`\`\`js${prefix}// @errors: 7031${code}\`\`\``;
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const code = await read_d_ts_file('types/index.d.ts');
|
||||
const node = ts.createSourceFile('index.d.ts', code, ts.ScriptTarget.Latest, true);
|
||||
|
||||
for (const statement of node.statements) {
|
||||
if (ts.isModuleDeclaration(statement)) {
|
||||
// @ts-ignore
|
||||
const name = statement.name.text || statement.name.escapedText;
|
||||
|
||||
const ignore_list = [
|
||||
'*.svelte',
|
||||
'svelte/types/compiler/preprocess', // legacy entrypoints, omit from docs
|
||||
'svelte/types/compiler/interfaces' // legacy entrypoints, omit from docs
|
||||
];
|
||||
if (ignore_list.includes(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const comment = strip_origin(statement.jsDoc?.[0].comment ?? '');
|
||||
|
||||
modules.push({
|
||||
name,
|
||||
comment,
|
||||
// @ts-ignore
|
||||
...(await get_types(code, statement.body?.statements))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modules.sort((a, b) => (a.name < b.name ? -1 : 1));
|
||||
|
||||
// Remove $$_attributes from ActionReturn
|
||||
$: {
|
||||
const module_with_ActionReturn = modules.find((m) =>
|
||||
m.types.find((t) => t?.name === 'ActionReturn')
|
||||
);
|
||||
|
||||
const new_children =
|
||||
module_with_ActionReturn?.types[1].children.filter((c) => c.name !== '$$_attributes') || [];
|
||||
|
||||
if (!module_with_ActionReturn) break $;
|
||||
|
||||
module_with_ActionReturn.types[1].children = new_children;
|
||||
}
|
||||
|
||||
try {
|
||||
await mkdir(new URL('../../src/lib/generated', import.meta.url), { recursive: true });
|
||||
} catch {}
|
||||
|
||||
writeFile(
|
||||
new URL('../../src/lib/generated/type-info.js', import.meta.url),
|
||||
`
|
||||
/* This file is generated by running \`pnpm generate\`
|
||||
in the sites/svelte.dev directory — do not edit it */
|
||||
export const modules = /** @type {import('@sveltejs/site-kit/markdown').Modules} */ (${JSON.stringify(
|
||||
modules,
|
||||
null,
|
||||
' '
|
||||
)});
|
||||
`.trim()
|
||||
);
|
@ -1,9 +0,0 @@
|
||||
import sh from 'shelljs';
|
||||
|
||||
sh.env['FORCE_UPDATE'] = process.argv.includes('--force=true');
|
||||
|
||||
Promise.all([
|
||||
sh.exec('node ./scripts/get_contributors.js'),
|
||||
sh.exec('node ./scripts/get_donors.js'),
|
||||
sh.exec('node ./scripts/update_template.js')
|
||||
]);
|
@ -1,43 +0,0 @@
|
||||
// @ts-check
|
||||
import { lstat, readFile, stat, writeFile } from 'node:fs/promises';
|
||||
import path, { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import sh from 'shelljs';
|
||||
|
||||
const force = process.env.FORCE_UPDATE === 'true';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
sh.cd(path.join(__dirname, '..'));
|
||||
|
||||
const outputFile = 'static/svelte-app.json';
|
||||
|
||||
try {
|
||||
if (!force && (await stat(outputFile))) {
|
||||
console.info(`[update/template] ${outputFile} exists. Skipping`);
|
||||
process.exit(0);
|
||||
}
|
||||
} catch {
|
||||
// fetch svelte app
|
||||
sh.rm('-rf', 'scripts/svelte-app');
|
||||
sh.exec('npx degit sveltejs/template scripts/svelte-app');
|
||||
|
||||
// remove src (will be recreated client-side) and node_modules
|
||||
sh.rm('-rf', 'scripts/svelte-app/src');
|
||||
sh.rm('-rf', 'scripts/svelte-app/node_modules');
|
||||
|
||||
// build svelte-app.json
|
||||
const appPath = 'scripts/svelte-app';
|
||||
const files = [];
|
||||
|
||||
for (const path of sh.find(appPath)) {
|
||||
// Skip directories
|
||||
if (!(await lstat(path)).isFile()) continue;
|
||||
|
||||
const bytes = await readFile(path);
|
||||
const string = bytes.toString();
|
||||
const data = bytes.compare(Buffer.from(string)) === 0 ? string : [...bytes];
|
||||
files.push({ path: path.slice(appPath.length + 1), data });
|
||||
}
|
||||
|
||||
writeFile(outputFile, JSON.stringify(files));
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
@ -1,45 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="theme-default typo-default">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="theme-color" content="#ff3e00" />
|
||||
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
|
||||
<meta name="twitter:site" content="@sveltejs" />
|
||||
<meta name="twitter:creator" content="@sveltejs" />
|
||||
|
||||
<!-- add inline style and blocking script to prevent content flash/jump -->
|
||||
<style>
|
||||
.ts-version {
|
||||
display: none;
|
||||
}
|
||||
.prefers-ts .js-version {
|
||||
display: none;
|
||||
}
|
||||
.prefers-ts .ts-version {
|
||||
display: block;
|
||||
}
|
||||
.no-js .ts-toggle {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-code="hover">
|
||||
<script>
|
||||
const themeValue = JSON.parse(localStorage.getItem('svelte:theme'))?.current;
|
||||
const systemPreferredTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
|
||||
document.body.classList.remove('light', 'dark');
|
||||
document.body.classList.add(themeValue ?? systemPreferredTheme);
|
||||
</script>
|
||||
|
||||
<div style="height: 100%">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
@ -1,6 +0,0 @@
|
||||
// REPL props
|
||||
|
||||
export const svelteUrl = `https://unpkg.com/svelte@4`;
|
||||
export const mapbox_setup = `window.MAPBOX_ACCESS_TOKEN = '${
|
||||
import.meta.env.VITE_MAPBOX_ACCESS_TOKEN
|
||||
}';`;
|
@ -1,9 +0,0 @@
|
||||
export const CONTENT_BASE = '../../documentation';
|
||||
|
||||
/** All the paths are relative to the project root when being run on server or built */
|
||||
export const CONTENT_BASE_PATHS = {
|
||||
BLOG: `${CONTENT_BASE}/blog`,
|
||||
TUTORIAL: `${CONTENT_BASE}/tutorial`,
|
||||
DOCS: `${CONTENT_BASE}/docs`,
|
||||
EXAMPLES: `${CONTENT_BASE}/examples`
|
||||
};
|
@ -1,13 +0,0 @@
|
||||
interface ImageToolsPictureData {
|
||||
sources: Record<'avif' | 'webp' | 'png', { src: string; w: number }[]>;
|
||||
img: {
|
||||
src: string;
|
||||
w: number;
|
||||
h: number;
|
||||
};
|
||||
}
|
||||
|
||||
declare module '*?big-image' {
|
||||
const value: ImageToolsPictureData;
|
||||
export default value;
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
/** @type {import('@sveltejs/kit').Handle} */
|
||||
export async function handle({ event, resolve }) {
|
||||
return await resolve(event, {
|
||||
preload: ({ type }) => type === 'js' || type === 'css' || type === 'font'
|
||||
});
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let once = false;
|
||||
export let top = 0;
|
||||
export let bottom = 0;
|
||||
export let left = 0;
|
||||
export let right = 0;
|
||||
|
||||
let intersecting = false;
|
||||
let container;
|
||||
|
||||
onMount(() => {
|
||||
if (typeof IntersectionObserver !== 'undefined') {
|
||||
const rootMargin = `${bottom}px ${left}px ${top}px ${right}px`;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
intersecting = entries[0].isIntersecting;
|
||||
if (intersecting && once) {
|
||||
observer.unobserve(container);
|
||||
}
|
||||
},
|
||||
{ rootMargin }
|
||||
);
|
||||
|
||||
observer.observe(container);
|
||||
return () => observer.unobserve(container);
|
||||
}
|
||||
|
||||
function handler() {
|
||||
const bcr = container.getBoundingClientRect();
|
||||
|
||||
intersecting =
|
||||
bcr.bottom + bottom > 0 &&
|
||||
bcr.right + right > 0 &&
|
||||
bcr.top - top < window.innerHeight &&
|
||||
bcr.left - left < window.innerWidth;
|
||||
|
||||
if (intersecting && once) {
|
||||
window.removeEventListener('scroll', handler);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', handler);
|
||||
return () => window.removeEventListener('scroll', handler);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={container}>
|
||||
<slot {intersecting} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
@ -1,11 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let constructor;
|
||||
|
||||
onMount(async () => {
|
||||
constructor = await $$props.this();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:component this={constructor} {...$$props} />
|
@ -1,67 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let p = 0;
|
||||
let visible = false;
|
||||
|
||||
onMount(() => {
|
||||
function next() {
|
||||
visible = true;
|
||||
p += 0.1;
|
||||
|
||||
const remaining = 1 - p;
|
||||
if (remaining > 0.15) setTimeout(next, 500 / remaining);
|
||||
}
|
||||
|
||||
setTimeout(next, 250);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<div class="progress-container">
|
||||
<div class="progress" style="inline-size: {p * 100}%" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if p >= 0.4}
|
||||
<div class="fade" />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.progress-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background-color: var(--sk-theme-1);
|
||||
transition: width 0.4s;
|
||||
}
|
||||
|
||||
.fade {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
pointer-events: none;
|
||||
z-index: 998;
|
||||
animation: fade 0.4s;
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,95 +0,0 @@
|
||||
<script>
|
||||
import { browser } from '$app/environment';
|
||||
import { process_example } from '$lib/utils/examples';
|
||||
import Repl from '@sveltejs/repl';
|
||||
import { theme } from '@sveltejs/site-kit/stores';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let version = '4';
|
||||
export let gist = null;
|
||||
export let example = null;
|
||||
export let embedded = false;
|
||||
|
||||
/** @type {import('@sveltejs/repl').default} */
|
||||
let repl;
|
||||
let name = 'loading...';
|
||||
|
||||
let mounted = false;
|
||||
|
||||
async function load(gist, example) {
|
||||
if (version !== 'local') {
|
||||
fetch(`https://unpkg.com/svelte@${version}/package.json`)
|
||||
.then((r) => r.json())
|
||||
.then((pkg) => {
|
||||
version = pkg.version;
|
||||
});
|
||||
}
|
||||
|
||||
if (gist) {
|
||||
fetch(`/repl/api/${gist}.json`)
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
const { description, components } = data;
|
||||
|
||||
name = description;
|
||||
|
||||
const files = Object.keys(components)
|
||||
.map((file) => {
|
||||
const dot = file.lastIndexOf('.');
|
||||
if (!~dot) return;
|
||||
|
||||
const source = components[file].content;
|
||||
|
||||
return {
|
||||
name: file.slice(0, dot),
|
||||
type: file.slice(dot + 1),
|
||||
source
|
||||
};
|
||||
})
|
||||
.filter((x) => x.type === 'svelte' || x.type === 'js')
|
||||
.sort((a, b) => {
|
||||
if (a.name === 'App' && a.type === 'svelte') return -1;
|
||||
if (b.name === 'App' && b.type === 'svelte') return 1;
|
||||
|
||||
if (a.type !== b.type) return a.type === 'svelte' ? -1 : 1;
|
||||
|
||||
return a.name < b.name ? -1 : 1;
|
||||
});
|
||||
|
||||
repl.set({ files });
|
||||
});
|
||||
} else if (example) {
|
||||
const files = process_example(
|
||||
(await fetch(`/examples/api/${example}.json`).then((r) => r.json())).files
|
||||
);
|
||||
|
||||
repl.set({
|
||||
files
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
});
|
||||
|
||||
$: if (mounted) load(gist, example);
|
||||
|
||||
$: if (embedded) document.title = `${name} • Svelte REPL`;
|
||||
|
||||
$: svelteUrl =
|
||||
browser && version === 'local'
|
||||
? `${location.origin}/repl/local`
|
||||
: `https://unpkg.com/svelte@${version}`;
|
||||
</script>
|
||||
|
||||
{#if browser}
|
||||
<Repl
|
||||
bind:this={repl}
|
||||
autocomplete={embedded}
|
||||
{svelteUrl}
|
||||
embedded
|
||||
relaxed
|
||||
previewTheme={$theme.current}
|
||||
/>
|
||||
{/if}
|
@ -1,43 +0,0 @@
|
||||
<script>
|
||||
export let labels;
|
||||
export let offset = 0;
|
||||
</script>
|
||||
|
||||
<div class="toggle">
|
||||
{#each labels as label, index}
|
||||
<button class:selected={offset === index} on:click={() => (offset = index)}>
|
||||
{label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toggle {
|
||||
position: fixed;
|
||||
bottom: var(--sk-nav-height);
|
||||
width: 100%;
|
||||
height: 4.6rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-top: 1px solid var(--sk-theme-2);
|
||||
background-color: var(--sk-back-4);
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0 0.15em;
|
||||
width: 4em;
|
||||
height: 1em;
|
||||
padding: 0.3em 0.4em;
|
||||
border-radius: var(--sk-border-radius);
|
||||
line-height: 1em;
|
||||
box-sizing: content-box;
|
||||
color: var(--sk-text-3);
|
||||
border: 1px solid var(--sk-back-3);
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: var(--sk-theme-1);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
@ -1,16 +0,0 @@
|
||||
import { dev } from '$app/environment';
|
||||
import { SUPABASE_URL, SUPABASE_KEY } from '$env/static/private';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const client_enabled = !!(!dev || (SUPABASE_URL && SUPABASE_KEY));
|
||||
|
||||
/**
|
||||
* @type {import('@supabase/supabase-js').SupabaseClient<any, "public", any>}
|
||||
*/
|
||||
// @ts-ignore-line
|
||||
export const client =
|
||||
client_enabled &&
|
||||
createClient(SUPABASE_URL, SUPABASE_KEY, {
|
||||
global: { fetch },
|
||||
auth: { persistSession: false }
|
||||
});
|
@ -1,107 +0,0 @@
|
||||
import { client } from './client.js';
|
||||
|
||||
/** @typedef {import('./types').User} User */
|
||||
/** @typedef {import('./types').UserID} UserID */
|
||||
/** @typedef {import('./types').Gist} Gist */
|
||||
|
||||
const PAGE_SIZE = 90;
|
||||
|
||||
/**
|
||||
* @param {User} user
|
||||
* @param {{
|
||||
* offset: number;
|
||||
* search: string | null;
|
||||
* }} opts
|
||||
*/
|
||||
export async function list(user, { offset, search }) {
|
||||
const { data, error } = await client.rpc('gist_list', {
|
||||
list_search: search || '',
|
||||
list_userid: user.id,
|
||||
list_count: PAGE_SIZE,
|
||||
list_start: offset
|
||||
});
|
||||
|
||||
if (error) throw new Error(error.message);
|
||||
|
||||
// normalize IDs
|
||||
data.forEach(
|
||||
/** @param {{id:string}} gist */ (gist) => {
|
||||
gist.id = gist.id.replace(/-/g, '');
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
gists: data.slice(0, PAGE_SIZE),
|
||||
next: data.length > PAGE_SIZE ? offset + PAGE_SIZE : null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {User} user
|
||||
* @param {Pick<Gist, 'name'|'files'>} gist
|
||||
* @returns {Promise<Gist>}
|
||||
*/
|
||||
export async function create(user, gist) {
|
||||
const { data, error } = await client.rpc('gist_create', {
|
||||
name: gist.name,
|
||||
files: gist.files,
|
||||
userid: user.id
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @returns {Promise<Partial<Gist>>}
|
||||
*/
|
||||
export async function read(id) {
|
||||
const { data, error } = await client
|
||||
.from('gist')
|
||||
.select('id,name,files,userid')
|
||||
.eq('id', id)
|
||||
.is('deleted_at', null);
|
||||
|
||||
if (error) throw new Error(error.message);
|
||||
return data[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {User} user
|
||||
* @param {string} gistid
|
||||
* @param {Pick<Gist, 'name'|'files'>} gist
|
||||
* @returns {Promise<Gist>}
|
||||
*/
|
||||
export async function update(user, gistid, gist) {
|
||||
const { data, error } = await client.rpc('gist_update', {
|
||||
gist_id: gistid,
|
||||
gist_name: gist.name,
|
||||
gist_files: gist.files,
|
||||
gist_userid: user.id
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} userid
|
||||
* @param {string[]} ids
|
||||
*/
|
||||
export async function destroy(userid, ids) {
|
||||
const { error } = await client.rpc('gist_destroy', {
|
||||
gist_ids: ids,
|
||||
gist_userid: userid
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
import * as cookie from 'cookie';
|
||||
import flru from 'flru';
|
||||
import { client } from './client.js';
|
||||
|
||||
/** @typedef {import('./types').User} User */
|
||||
|
||||
/**
|
||||
* @type {import('flru').flruCache<User | null>}
|
||||
*/
|
||||
const session_cache = flru(1000);
|
||||
|
||||
/**
|
||||
* @param {import('./types').GitHubUser} user
|
||||
*/
|
||||
export async function create(user) {
|
||||
const { data, error } = await client.rpc('login', {
|
||||
user_github_id: user.github_id,
|
||||
user_github_name: user.github_name,
|
||||
user_github_login: user.github_login,
|
||||
user_github_avatar_url: user.github_avatar_url
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
session_cache.set(data.sessionid, {
|
||||
id: data.userid,
|
||||
github_name: user.github_name,
|
||||
github_login: user.github_login,
|
||||
github_avatar_url: user.github_avatar_url
|
||||
});
|
||||
|
||||
return {
|
||||
sessionid: data.sessionid,
|
||||
expires: new Date(data.expires)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sessionid
|
||||
* @returns {Promise<User | null>}
|
||||
*/
|
||||
export async function read(sessionid) {
|
||||
if (!sessionid) return null;
|
||||
|
||||
if (!session_cache.get(sessionid)) {
|
||||
session_cache.set(
|
||||
sessionid,
|
||||
await client.rpc('get_user', { sessionid }).then(({ data, error }) => {
|
||||
if (error) {
|
||||
session_cache.set(sessionid, null);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
return data.id && data;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return session_cache.get(sessionid) || null;
|
||||
}
|
||||
|
||||
/** @param {string} sessionid */
|
||||
export async function destroy(sessionid) {
|
||||
const { error } = await client.rpc('logout', { sessionid });
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
session_cache.set(sessionid, null);
|
||||
}
|
||||
|
||||
/** @param {string | null} str */
|
||||
export function from_cookie(str) {
|
||||
if (!str) return null;
|
||||
return read(cookie.parse(str).sid);
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
export type UserID = number;
|
||||
|
||||
export interface User {
|
||||
id: UserID;
|
||||
github_name: string;
|
||||
github_login: string;
|
||||
github_avatar_url: string;
|
||||
}
|
||||
|
||||
export interface GitHubUser {
|
||||
github_id: string;
|
||||
github_name: string;
|
||||
github_login: string;
|
||||
github_avatar_url: string;
|
||||
}
|
||||
|
||||
export interface Gist {
|
||||
id: string;
|
||||
name: string;
|
||||
owner: UserID;
|
||||
files: Array<{ name: string; type: string; source: string }>;
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
// @ts-check
|
||||
import { extractFrontmatter } from '@sveltejs/site-kit/markdown';
|
||||
import { CONTENT_BASE_PATHS } from '../../../constants.js';
|
||||
import { render_content } from '../renderer.js';
|
||||
import { get_sections } from '../docs/index.js';
|
||||
|
||||
/**
|
||||
* @param {import('./types').BlogData} blog_data
|
||||
* @param {string} slug
|
||||
*/
|
||||
export async function get_processed_blog_post(blog_data, slug) {
|
||||
for (const post of blog_data) {
|
||||
if (post.slug === slug) {
|
||||
return {
|
||||
...post,
|
||||
content: await render_content(post.file, post.content)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const BLOG_NAME_REGEX = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
|
||||
|
||||
/** @returns {Promise<import('./types').BlogData>} */
|
||||
export async function get_blog_data(base = CONTENT_BASE_PATHS.BLOG) {
|
||||
const { readdir, readFile } = await import('node:fs/promises');
|
||||
|
||||
/** @type {import('./types').BlogData} */
|
||||
const blog_posts = [];
|
||||
|
||||
for (const file of (await readdir(base)).reverse()) {
|
||||
if (!BLOG_NAME_REGEX.test(file)) continue;
|
||||
|
||||
const { date, date_formatted, slug } = get_date_and_slug(file);
|
||||
const { metadata, body } = extractFrontmatter(await readFile(`${base}/${file}`, 'utf-8'));
|
||||
|
||||
blog_posts.push({
|
||||
date,
|
||||
date_formatted,
|
||||
content: body,
|
||||
description: metadata.description,
|
||||
draft: metadata.draft === 'true',
|
||||
slug,
|
||||
title: metadata.title,
|
||||
file,
|
||||
author: {
|
||||
name: metadata.author,
|
||||
url: metadata.authorURL
|
||||
},
|
||||
sections: await get_sections(body)
|
||||
});
|
||||
}
|
||||
|
||||
return blog_posts;
|
||||
}
|
||||
|
||||
/** @param {import('./types').BlogData} blog_data */
|
||||
export function get_blog_list(blog_data) {
|
||||
return blog_data.map(({ slug, date, title, description, draft }) => ({
|
||||
slug,
|
||||
date,
|
||||
title,
|
||||
description,
|
||||
draft
|
||||
}));
|
||||
}
|
||||
|
||||
/** @param {string} filename */
|
||||
function get_date_and_slug(filename) {
|
||||
const match = BLOG_NAME_REGEX.exec(filename);
|
||||
if (!match) throw new Error(`Invalid filename for blog: '${filename}'`);
|
||||
|
||||
const [, date, slug] = match;
|
||||
const [y, m, d] = date.split('-');
|
||||
const date_formatted = `${months[+m - 1]} ${+d} ${y}`;
|
||||
|
||||
return { date, date_formatted, slug };
|
||||
}
|
||||
|
||||
const months = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ');
|
@ -1,27 +0,0 @@
|
||||
import type { Section } from '../docs/types';
|
||||
|
||||
export interface BlogPost {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
date_formatted: string;
|
||||
slug: string;
|
||||
file: string;
|
||||
author: {
|
||||
name: string;
|
||||
url?: string;
|
||||
};
|
||||
draft: boolean;
|
||||
content: string;
|
||||
sections: Section[];
|
||||
}
|
||||
|
||||
export type BlogData = BlogPost[];
|
||||
|
||||
export interface BlogPostSummary {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
draft: boolean;
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
import { base as app_base } from '$app/paths';
|
||||
import {
|
||||
escape,
|
||||
extractFrontmatter,
|
||||
markedTransform,
|
||||
normalizeSlugify,
|
||||
removeMarkdown
|
||||
} from '@sveltejs/site-kit/markdown';
|
||||
import { CONTENT_BASE_PATHS } from '../../../constants.js';
|
||||
import { render_content } from '../renderer';
|
||||
|
||||
/**
|
||||
* @param {import('./types').DocsData} docs_data
|
||||
* @param {string} slug
|
||||
*/
|
||||
export async function get_parsed_docs(docs_data, slug) {
|
||||
for (const { pages } of docs_data) {
|
||||
for (const page of pages) {
|
||||
if (page.slug === slug) {
|
||||
return {
|
||||
...page,
|
||||
content: await render_content(page.file, page.content)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return {Promise<import('./types').DocsData>} */
|
||||
export async function get_docs_data(base = CONTENT_BASE_PATHS.DOCS) {
|
||||
const { readdir, readFile } = await import('node:fs/promises');
|
||||
|
||||
/** @type {import('./types').DocsData} */
|
||||
const docs_data = [];
|
||||
|
||||
for (const category_dir of await readdir(base)) {
|
||||
const match = /\d{2}-(.+)/.exec(category_dir);
|
||||
if (!match) continue;
|
||||
|
||||
const category_slug = match[1];
|
||||
|
||||
// Read the meta.json
|
||||
const { title: category_title, draft = 'false' } = JSON.parse(
|
||||
await readFile(`${base}/${category_dir}/meta.json`, 'utf-8')
|
||||
);
|
||||
|
||||
if (draft === 'true') continue;
|
||||
|
||||
/** @type {import('./types').Category} */
|
||||
const category = {
|
||||
title: category_title,
|
||||
slug: category_slug,
|
||||
pages: []
|
||||
};
|
||||
|
||||
for (const filename of await readdir(`${base}/${category_dir}`)) {
|
||||
if (filename === 'meta.json') continue;
|
||||
const match = /\d{2}-(.+)/.exec(filename);
|
||||
if (!match) continue;
|
||||
|
||||
const page_slug = match[1].replace('.md', '');
|
||||
|
||||
const page_data = extractFrontmatter(
|
||||
await readFile(`${base}/${category_dir}/${filename}`, 'utf-8')
|
||||
);
|
||||
|
||||
if (page_data.metadata.draft === 'true') continue;
|
||||
|
||||
const page_title = page_data.metadata.title;
|
||||
const page_content = page_data.body;
|
||||
|
||||
category.pages.push({
|
||||
title: page_title,
|
||||
slug: page_slug,
|
||||
content: page_content,
|
||||
category: category_title,
|
||||
sections: await get_sections(page_content),
|
||||
path: `${app_base}/docs/${page_slug}`,
|
||||
file: `${category_dir}/${filename}`
|
||||
});
|
||||
}
|
||||
|
||||
docs_data.push(category);
|
||||
}
|
||||
|
||||
return docs_data;
|
||||
}
|
||||
|
||||
/** @param {import('./types').DocsData} docs_data */
|
||||
export function get_docs_list(docs_data) {
|
||||
return docs_data.map((category) => ({
|
||||
title: category.title,
|
||||
pages: category.pages.map((page) => ({
|
||||
title: page.title,
|
||||
path: page.path
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
/** @param {string} str */
|
||||
const titled = async (str) =>
|
||||
removeMarkdown(
|
||||
escape(await markedTransform(str, { paragraph: (txt) => txt }))
|
||||
.replace(/<\/?code>/g, '')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/, '&')
|
||||
.replace(/<(\/)?(em|b|strong|code)>/g, '')
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {string} markdown
|
||||
* @returns {Promise<import('./types').Section[]>}
|
||||
*/
|
||||
export async function get_sections(markdown) {
|
||||
const lines = markdown.split('\n');
|
||||
const root = /** @type {import('./types').Section} */ ({
|
||||
title: 'Root',
|
||||
slug: 'root',
|
||||
sections: [],
|
||||
breadcrumbs: [''],
|
||||
text: ''
|
||||
});
|
||||
let currentNodes = [root];
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^(#{2,4})\s(.*)/);
|
||||
if (match) {
|
||||
const level = match[1].length - 2;
|
||||
const text = await titled(match[2]);
|
||||
const slug = normalizeSlugify(text);
|
||||
|
||||
// Prepare new node
|
||||
/** @type {import('./types').Section} */
|
||||
const newNode = {
|
||||
title: text,
|
||||
slug,
|
||||
sections: [],
|
||||
breadcrumbs: [...currentNodes[level].breadcrumbs, text],
|
||||
text: ''
|
||||
};
|
||||
|
||||
// Add the new node to the tree
|
||||
const sections = currentNodes[level].sections;
|
||||
if (!sections) throw new Error(`Could not find section ${level}`);
|
||||
sections.push(newNode);
|
||||
|
||||
// Prepare for potential children of the new node
|
||||
currentNodes = currentNodes.slice(0, level + 1);
|
||||
currentNodes.push(newNode);
|
||||
} else if (line.trim() !== '') {
|
||||
// Add non-heading line to the text of the current section
|
||||
currentNodes[currentNodes.length - 1].text += line + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
return /** @type {import('./types').Section[]} */ (root.sections);
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
export type DocsData = Category[];
|
||||
|
||||
export interface Section {
|
||||
title: string;
|
||||
slug: string;
|
||||
// Currently, we are only going with 2 level headings, so this will be undefined. In future, we may want to support 3 levels, in which case this will be a list of sections
|
||||
sections?: Section[];
|
||||
breadcrumbs: string[];
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type Category = {
|
||||
title: string;
|
||||
slug: string;
|
||||
pages: Page[];
|
||||
};
|
||||
|
||||
export type Page = {
|
||||
title: string;
|
||||
category: string;
|
||||
slug: string;
|
||||
file: string;
|
||||
path: string;
|
||||
content: string;
|
||||
sections: Section[];
|
||||
};
|
@ -1,98 +0,0 @@
|
||||
import { CONTENT_BASE_PATHS } from '../../../constants.js';
|
||||
|
||||
/**
|
||||
* @param {import('./types').ExamplesData} examples_data
|
||||
* @param {string} slug
|
||||
*/
|
||||
export function get_example(examples_data, slug) {
|
||||
for (const section of examples_data) {
|
||||
for (const example of section.examples) {
|
||||
if (example.slug === slug) {
|
||||
return example;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<import('./types').ExamplesData>}
|
||||
*/
|
||||
export async function get_examples_data(base = CONTENT_BASE_PATHS.EXAMPLES) {
|
||||
const { readdir, stat, readFile } = await import('node:fs/promises');
|
||||
|
||||
const examples = [];
|
||||
|
||||
for (const subdir of await readdir(base)) {
|
||||
/** @type {import('./types').ExamplesDatum} */
|
||||
const section = {
|
||||
title: '', // Initialise with empty
|
||||
slug: subdir.split('-').slice(1).join('-'),
|
||||
examples: []
|
||||
};
|
||||
|
||||
if (!((await stat(`${base}/${subdir}`)).isDirectory() || subdir.endsWith('meta.json')))
|
||||
continue;
|
||||
|
||||
if (!subdir.endsWith('meta.json'))
|
||||
section.title =
|
||||
JSON.parse(await readFile(`${base}/${subdir}/meta.json`, 'utf-8')).title ?? 'Embeds';
|
||||
|
||||
for (const section_dir of await readdir(`${base}/${subdir}`)) {
|
||||
const match = /\d{2}-(.+)/.exec(section_dir);
|
||||
if (!match) continue;
|
||||
|
||||
const slug = match[1];
|
||||
|
||||
const example_base_dir = `${base}/${subdir}/${section_dir}`;
|
||||
|
||||
// Get title for
|
||||
const example_title = JSON.parse(
|
||||
await readFile(`${example_base_dir}/meta.json`, 'utf-8')
|
||||
).title;
|
||||
|
||||
/**
|
||||
* @type {Array<{
|
||||
* name: string;
|
||||
* type: string;
|
||||
* content: string;
|
||||
* }>}
|
||||
*/
|
||||
const files = [];
|
||||
for (const file of (await readdir(example_base_dir)).filter(
|
||||
(file) => !file.endsWith('meta.json')
|
||||
)) {
|
||||
const type = file.split('.').at(-1);
|
||||
if (!type) {
|
||||
throw new Error(`Could not determine type from ${file}`);
|
||||
}
|
||||
files.push({
|
||||
name: file,
|
||||
type,
|
||||
content: await readFile(`${example_base_dir}/${file}`, 'utf-8')
|
||||
});
|
||||
}
|
||||
|
||||
section.examples.push({ title: example_title, slug, files });
|
||||
}
|
||||
|
||||
examples.push(section);
|
||||
}
|
||||
|
||||
return examples;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./types').ExamplesData} examples_data
|
||||
* @returns {import('./types').ExamplesList}
|
||||
*/
|
||||
export function get_examples_list(examples_data) {
|
||||
return examples_data.map((section) => ({
|
||||
title: section.title,
|
||||
examples: section.examples.map((example) => ({
|
||||
title: example.title,
|
||||
slug: example.slug
|
||||
}))
|
||||
}));
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
export interface ExamplesDatum {
|
||||
title: string;
|
||||
slug: string;
|
||||
examples: {
|
||||
title: string;
|
||||
slug: string;
|
||||
files: {
|
||||
content: string;
|
||||
type: string;
|
||||
name: string;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export type ExamplesData = ExamplesDatum[];
|
||||
|
||||
export interface Example {
|
||||
title: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface ExampleSection {
|
||||
title: string;
|
||||
examples: Example[];
|
||||
}
|
||||
|
||||
export type ExamplesList = ExampleSection[];
|
@ -1,52 +0,0 @@
|
||||
import { modules } from '$lib/generated/type-info';
|
||||
import { renderContentMarkdown, slugify } from '@sveltejs/site-kit/markdown';
|
||||
|
||||
/**
|
||||
* @param {string} filename
|
||||
* @param {string} body
|
||||
* @returns
|
||||
*/
|
||||
export const render_content = (filename, body) =>
|
||||
renderContentMarkdown(filename, body, {
|
||||
cacheCodeSnippets: true,
|
||||
modules,
|
||||
|
||||
resolveTypeLinks: (module_name, type_name) => {
|
||||
return {
|
||||
page: `/docs/${slugify(module_name)}`,
|
||||
slug: `types-${slugify(type_name)}`
|
||||
};
|
||||
},
|
||||
|
||||
twoslashBanner: (filename, source) => {
|
||||
const injected = [];
|
||||
|
||||
if (/(svelte)/.test(source) || filename.includes('typescript')) {
|
||||
injected.push(`// @filename: ambient.d.ts`, `/// <reference types="svelte" />`);
|
||||
}
|
||||
|
||||
if (filename.includes('svelte-compiler')) {
|
||||
injected.push('// @esModuleInterop');
|
||||
}
|
||||
|
||||
if (filename.includes('svelte.md')) {
|
||||
injected.push('// @errors: 2304');
|
||||
}
|
||||
|
||||
// Actions JSDoc examples are invalid. Too many errors, edge cases
|
||||
if (filename.includes('svelte-action')) {
|
||||
injected.push('// @noErrors');
|
||||
}
|
||||
|
||||
if (filename.includes('typescript')) {
|
||||
injected.push('// @errors: 2304');
|
||||
}
|
||||
|
||||
// Tutorials
|
||||
if (filename.startsWith('tutorial')) {
|
||||
injected.push('// @noErrors');
|
||||
}
|
||||
|
||||
return injected.join('\n');
|
||||
}
|
||||
});
|
@ -1,112 +0,0 @@
|
||||
import { extractFrontmatter } from '@sveltejs/site-kit/markdown';
|
||||
import { CONTENT_BASE_PATHS } from '../../../constants.js';
|
||||
import { render_content } from '../renderer.js';
|
||||
|
||||
/**
|
||||
* @param {import('./types').TutorialData} tutorial_data
|
||||
* @param {string} slug
|
||||
*/
|
||||
export async function get_parsed_tutorial(tutorial_data, slug) {
|
||||
for (const { tutorials } of tutorial_data) {
|
||||
for (const tutorial of tutorials) {
|
||||
if (tutorial.slug === slug) {
|
||||
return {
|
||||
...tutorial,
|
||||
content: await render_content(`tutorial/${tutorial.dir}`, tutorial.content)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<import('./types').TutorialData>}
|
||||
*/
|
||||
export async function get_tutorial_data(base = CONTENT_BASE_PATHS.TUTORIAL) {
|
||||
const { readdir, readFile, stat } = await import('node:fs/promises');
|
||||
|
||||
const tutorials = [];
|
||||
|
||||
for (const subdir of await readdir(base)) {
|
||||
/** @type {import('./types').TutorialDatum} */
|
||||
const section = {
|
||||
title: '', // Initialise with empty
|
||||
slug: subdir.split('-').slice(1).join('-'),
|
||||
tutorials: []
|
||||
};
|
||||
|
||||
if (!((await stat(`${base}/${subdir}`)).isDirectory() || subdir.endsWith('meta.json')))
|
||||
continue;
|
||||
|
||||
if (!subdir.endsWith('meta.json'))
|
||||
section.title = JSON.parse(await readFile(`${base}/${subdir}/meta.json`, 'utf-8')).title;
|
||||
|
||||
for (const section_dir of await readdir(`${base}/${subdir}`)) {
|
||||
const match = /\d{2}-(.+)/.exec(section_dir);
|
||||
if (!match) continue;
|
||||
|
||||
const slug = match[1];
|
||||
|
||||
const tutorial_base_dir = `${base}/${subdir}/${section_dir}`;
|
||||
|
||||
// Read the file, get frontmatter
|
||||
const contents = await readFile(`${tutorial_base_dir}/text.md`, 'utf-8');
|
||||
const { metadata, body } = extractFrontmatter(contents);
|
||||
|
||||
// Get the contents of the apps.
|
||||
/**
|
||||
* @type {{
|
||||
* initial: import('./types').CompletionState[];
|
||||
* complete: import('./types').CompletionState[];
|
||||
* }}
|
||||
*/
|
||||
const completion_states_data = { initial: [], complete: [] };
|
||||
for (const app_dir of await readdir(tutorial_base_dir)) {
|
||||
if (!app_dir.startsWith('app-')) continue;
|
||||
|
||||
const app_dir_path = `${tutorial_base_dir}/${app_dir}`;
|
||||
const app_contents = await readdir(app_dir_path, 'utf-8');
|
||||
|
||||
for (const file of app_contents) {
|
||||
const type = file.split('.').at(-1);
|
||||
if (!type) {
|
||||
throw new Error(`Could not determine type from ${file}`);
|
||||
}
|
||||
completion_states_data[app_dir === 'app-a' ? 'initial' : 'complete'].push({
|
||||
name: file,
|
||||
type,
|
||||
content: await readFile(`${app_dir_path}/${file}`, 'utf-8')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
section.tutorials.push({
|
||||
title: metadata.title,
|
||||
slug,
|
||||
content: body,
|
||||
dir: `${subdir}/${section_dir}`,
|
||||
...completion_states_data
|
||||
});
|
||||
}
|
||||
|
||||
tutorials.push(section);
|
||||
}
|
||||
|
||||
return tutorials;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./types').TutorialData} tutorial_data
|
||||
* @returns {import('./types').TutorialsList}
|
||||
*/
|
||||
export function get_tutorial_list(tutorial_data) {
|
||||
return tutorial_data.map((section) => ({
|
||||
title: section.title,
|
||||
tutorials: section.tutorials.map((tutorial) => ({
|
||||
title: tutorial.title,
|
||||
slug: tutorial.slug
|
||||
}))
|
||||
}));
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
export interface TutorialDatum {
|
||||
title: string;
|
||||
slug: string;
|
||||
tutorials: {
|
||||
title: string;
|
||||
slug: string;
|
||||
dir: string;
|
||||
content: string;
|
||||
initial: CompletionState[];
|
||||
complete: CompletionState[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface CompletionState {
|
||||
name: string;
|
||||
type: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type TutorialData = TutorialDatum[];
|
||||
|
||||
export interface Tutorial {
|
||||
title: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface TutorialSection {
|
||||
title: string;
|
||||
tutorials: Tutorial[];
|
||||
}
|
||||
|
||||
export type TutorialsList = TutorialSection[];
|
@ -1,28 +0,0 @@
|
||||
const formatter = new Intl.RelativeTimeFormat(undefined, {
|
||||
numeric: 'auto'
|
||||
});
|
||||
|
||||
const DIVISIONS = {
|
||||
seconds: 60,
|
||||
minutes: 60,
|
||||
hours: 24,
|
||||
days: 7,
|
||||
weeks: 4.34524,
|
||||
months: 12,
|
||||
years: Number.POSITIVE_INFINITY
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Date} date
|
||||
*/
|
||||
export const ago = (date) => {
|
||||
let duration = (date.getTime() - new Date().getTime()) / 1000;
|
||||
|
||||
for (const [name, amount] of Object.entries(DIVISIONS)) {
|
||||
if (Math.abs(duration) < amount) {
|
||||
const format = /** @type {keyof(DIVISIONS)} */ (name);
|
||||
return formatter.format(Math.round(duration), format);
|
||||
}
|
||||
duration /= amount;
|
||||
}
|
||||
};
|
@ -1 +0,0 @@
|
||||
export const isMac = typeof navigator !== 'undefined' && navigator.platform === 'MacIntel';
|
@ -1,25 +0,0 @@
|
||||
/** @param {number} code */
|
||||
export function keyEvent(code) {
|
||||
/**
|
||||
* @param {HTMLInputElement} node
|
||||
* @param {(event: KeyboardEvent) => void} callback
|
||||
*/
|
||||
return function (node, callback) {
|
||||
node.addEventListener('keydown', handleKeydown);
|
||||
|
||||
/** @param {KeyboardEvent} event */
|
||||
function handleKeydown(event) {
|
||||
if (event.keyCode === code) {
|
||||
callback.call(this, event);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener('keydown', handleKeydown);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const enter = keyEvent(13);
|
@ -1,26 +0,0 @@
|
||||
/**
|
||||
* @param {Array<{
|
||||
* content: string;
|
||||
* name: string;
|
||||
* source: string;
|
||||
* type: string;
|
||||
* }>} files
|
||||
*/
|
||||
export function process_example(files) {
|
||||
return files
|
||||
.map((file) => {
|
||||
const [name, type] = file.name.split('.');
|
||||
return { name, type, source: file.source ?? file.content ?? '' };
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.name === 'App' && a.type === 'svelte') return -1;
|
||||
if (b.name === 'App' && b.type === 'svelte') return 1;
|
||||
|
||||
if (a.type === b.type) return a.name < b.name ? -1 : 1;
|
||||
|
||||
if (a.type === 'svelte') return -1;
|
||||
if (b.type === 'svelte') return 1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import * as session from '$lib/db/session';
|
||||
|
||||
/** @type {import('@sveltejs/adapter-vercel').Config} */
|
||||
export const config = {
|
||||
runtime: 'nodejs18.x' // see https://github.com/sveltejs/svelte/pull/9136
|
||||
};
|
||||
|
||||
export async function load({ request }) {
|
||||
return {
|
||||
user: await session.from_cookie(request.headers.get('cookie'))
|
||||
};
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
<script>
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { setContext } from 'svelte';
|
||||
|
||||
setContext('app', {
|
||||
login: () => {
|
||||
const login_window = window.open(
|
||||
`${window.location.origin}/auth/login`,
|
||||
'login',
|
||||
'width=600,height=400'
|
||||
);
|
||||
|
||||
window.addEventListener('message', function handler(event) {
|
||||
login_window.close();
|
||||
window.removeEventListener('message', handler);
|
||||
invalidateAll();
|
||||
});
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
const r = await fetch(`/auth/logout`);
|
||||
if (r.ok) invalidateAll();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<slot />
|
@ -1,20 +0,0 @@
|
||||
import * as gist from '$lib/db/gist';
|
||||
|
||||
export async function load({ url, parent }) {
|
||||
let gists = [];
|
||||
let next = null;
|
||||
|
||||
const search = url.searchParams.get('search');
|
||||
|
||||
const { user } = await parent();
|
||||
|
||||
if (user) {
|
||||
const offset_param = url.searchParams.get('offset');
|
||||
const offset = offset_param ? parseInt(offset_param) : 0;
|
||||
const search = url.searchParams.get('search');
|
||||
|
||||
({ gists, next } = await gist.list(user, { offset, search }));
|
||||
}
|
||||
|
||||
return { user, gists, next, search };
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import * as session from '$lib/db/session';
|
||||
import * as gist from '$lib/db/gist';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const user = await session.from_cookie(request.headers.get('cookie'));
|
||||
if (!user) return new Response(undefined, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
await gist.destroy(user.id, body.ids);
|
||||
|
||||
return new Response(undefined, { status: 204 });
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load({ url }) {
|
||||
const query = url.searchParams;
|
||||
const gist = query.get('gist');
|
||||
const example = query.get('example');
|
||||
const version = query.get('version');
|
||||
const vim = query.get('vim');
|
||||
|
||||
// redirect to v2 REPL if appropriate
|
||||
if (version && /^[^>]?[12]/.test(version)) {
|
||||
redirect(302, `https://v2.svelte.dev/repl?${query}`);
|
||||
}
|
||||
|
||||
const id = gist || example || 'hello-world';
|
||||
// we need to filter out null values
|
||||
const q = new URLSearchParams();
|
||||
if (version) q.set('version', version);
|
||||
if (vim) q.set('vim', vim);
|
||||
redirect(301, `/repl/${id}?${q}`);
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export function load({ data, url }) {
|
||||
// initialize vim with the search param
|
||||
const vim_search_params = url.searchParams.get('vim');
|
||||
let vim = vim_search_params !== null && vim_search_params !== 'false';
|
||||
// when in the browser check if there's a local storage entry and eventually override
|
||||
// vim if there's not a search params otherwise update the local storage
|
||||
if (browser) {
|
||||
const vim_local_storage = window.localStorage.getItem('svelte:vim-enabled');
|
||||
if (vim_search_params !== null) {
|
||||
window.localStorage.setItem('svelte:vim-enabled', vim.toString());
|
||||
} else if (vim_local_storage) {
|
||||
vim = vim_local_storage !== 'false';
|
||||
}
|
||||
}
|
||||
return { ...data, vim };
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export async function load({ fetch, params, url }) {
|
||||
const res = await fetch(`/repl/api/${params.id}.json`);
|
||||
|
||||
if (!res.ok) {
|
||||
error(/** @type {any} */ (res.status)); // TODO loosen the types so we can get rid of this
|
||||
}
|
||||
|
||||
const gist = await res.json();
|
||||
|
||||
return {
|
||||
gist,
|
||||
version: url.searchParams.get('version') || '4'
|
||||
};
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
<script>
|
||||
import { browser } from '$app/environment';
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
import Repl from '@sveltejs/repl';
|
||||
import { theme } from '@sveltejs/site-kit/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { mapbox_setup } from '../../../../config.js';
|
||||
import AppControls from './AppControls.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
let version = data.version;
|
||||
|
||||
/** @type {import('@sveltejs/repl').default} */
|
||||
let repl;
|
||||
let name = data.gist.name;
|
||||
let zen_mode = false;
|
||||
let modified_count = 0;
|
||||
|
||||
function update_query_string(version) {
|
||||
const params = [];
|
||||
|
||||
if (version !== 'latest') params.push(`version=${version}`);
|
||||
|
||||
const url =
|
||||
params.length > 0 ? `/repl/${data.gist.id}?${params.join('&')}` : `/repl/${data.gist.id}`;
|
||||
|
||||
history.replaceState({}, 'x', url);
|
||||
}
|
||||
|
||||
$: if (typeof history !== 'undefined') update_query_string(version);
|
||||
|
||||
onMount(() => {
|
||||
if (data.version !== 'local') {
|
||||
fetch(`https://unpkg.com/svelte@${data.version || '4'}/package.json`)
|
||||
.then((r) => r.json())
|
||||
.then((pkg) => {
|
||||
version = pkg.version;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
afterNavigate(() => {
|
||||
repl?.set({
|
||||
files: data.gist.components
|
||||
});
|
||||
});
|
||||
|
||||
function handle_fork(event) {
|
||||
console.log('> handle_fork', event);
|
||||
goto(`/repl/${event.detail.gist.id}?version=${version}`);
|
||||
}
|
||||
|
||||
function handle_change(event) {
|
||||
modified_count = event.detail.files.filter((c) => c.modified).length;
|
||||
}
|
||||
|
||||
$: svelteUrl =
|
||||
browser && version === 'local'
|
||||
? `${location.origin}/repl/local`
|
||||
: `https://unpkg.com/svelte@${version}`;
|
||||
|
||||
$: relaxed = data.gist.relaxed || (data.user && data.user.id === data.gist.owner);
|
||||
|
||||
$: vim = data.vim;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{name} • REPL • Svelte</title>
|
||||
|
||||
<meta name="twitter:title" content="{data.gist.name} • REPL • Svelte" />
|
||||
<meta name="twitter:description" content="Cybernetically enhanced web apps" />
|
||||
<meta name="description" content="Interactive Svelte playground" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="repl-outer {zen_mode ? 'zen-mode' : ''}">
|
||||
<AppControls
|
||||
user={data.user}
|
||||
gist={data.gist}
|
||||
{repl}
|
||||
bind:name
|
||||
bind:zen_mode
|
||||
bind:modified_count
|
||||
on:forked={handle_fork}
|
||||
/>
|
||||
|
||||
{#if browser}
|
||||
<Repl
|
||||
bind:this={repl}
|
||||
{svelteUrl}
|
||||
{relaxed}
|
||||
{vim}
|
||||
injectedJS={mapbox_setup}
|
||||
showModified
|
||||
showAst
|
||||
on:change={handle_change}
|
||||
on:add={handle_change}
|
||||
on:remove={handle_change}
|
||||
previewTheme={$theme.current}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.repl-outer {
|
||||
position: relative;
|
||||
height: calc(100% - var(--sk-nav-height) - var(--sk-banner-bottom-height));
|
||||
height: calc(100dvh - var(--sk-nav-height) - var(--sk-banner-bottom-height));
|
||||
--app-controls-h: 5.6rem;
|
||||
--pane-controls-h: 4.2rem;
|
||||
overflow: hidden;
|
||||
background-color: var(--sk-back-1);
|
||||
padding: var(--app-controls-h) 0 0 0;
|
||||
/* margin: 0 calc(var(--side-nav) * -1); */
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* temp fix for #2499 and #2550 while waiting for a fix for https://github.com/sveltejs/svelte-repl/issues/8 */
|
||||
|
||||
.repl-outer :global(.tab-content),
|
||||
.repl-outer :global(.tab-content.visible) {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
.repl-outer :global(.tab-content) {
|
||||
visibility: hidden;
|
||||
}
|
||||
.repl-outer :global(.tab-content.visible) {
|
||||
visibility: visible;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.zen-mode {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
z-index: 111;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,339 +0,0 @@
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from 'svelte';
|
||||
import UserMenu from './UserMenu.svelte';
|
||||
import { Icon } from '@sveltejs/site-kit/components';
|
||||
import * as doNotZip from 'do-not-zip';
|
||||
import downloadBlob from './downloadBlob.js';
|
||||
import { enter } from '$lib/utils/events.js';
|
||||
import { isMac } from '$lib/utils/compat.js';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const { login } = getContext('app');
|
||||
|
||||
export let user;
|
||||
|
||||
/** @type {import('@sveltejs/repl').default} */
|
||||
export let repl;
|
||||
export let gist;
|
||||
export let name;
|
||||
export let zen_mode;
|
||||
export let modified_count;
|
||||
|
||||
let saving = false;
|
||||
let downloading = false;
|
||||
let justSaved = false;
|
||||
let justForked = false;
|
||||
|
||||
function wait(ms) {
|
||||
return new Promise((f) => setTimeout(f, ms));
|
||||
}
|
||||
|
||||
$: canSave = user && gist && gist.owner === user.id;
|
||||
|
||||
function handleKeydown(event) {
|
||||
if (event.key === 's' && (isMac ? event.metaKey : event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
async function fork(intentWasSave) {
|
||||
saving = true;
|
||||
|
||||
const { files } = repl.toJSON();
|
||||
|
||||
try {
|
||||
const r = await fetch(`/repl/create.json`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
files: files.map((file) => ({
|
||||
name: `${file.name}.${file.type}`,
|
||||
source: file.source
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
if (r.status < 200 || r.status >= 300) {
|
||||
const { error } = await r.json();
|
||||
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
|
||||
}
|
||||
|
||||
const gist = await r.json();
|
||||
dispatch('forked', { gist });
|
||||
|
||||
modified_count = 0;
|
||||
repl.markSaved();
|
||||
|
||||
if (intentWasSave) {
|
||||
justSaved = true;
|
||||
await wait(600);
|
||||
justSaved = false;
|
||||
} else {
|
||||
justForked = true;
|
||||
await wait(600);
|
||||
justForked = false;
|
||||
}
|
||||
} catch (err) {
|
||||
if (navigator.onLine) {
|
||||
alert(err.message);
|
||||
} else {
|
||||
alert(`It looks like you're offline! Find the internet and try again`);
|
||||
}
|
||||
}
|
||||
|
||||
saving = false;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!user) {
|
||||
alert('Please log in before saving your app');
|
||||
return;
|
||||
}
|
||||
if (saving) return;
|
||||
|
||||
if (!canSave) {
|
||||
fork(true);
|
||||
return;
|
||||
}
|
||||
|
||||
saving = true;
|
||||
|
||||
try {
|
||||
// Send all files back to API
|
||||
// ~> Any missing files are considered deleted!
|
||||
const { files } = repl.toJSON();
|
||||
|
||||
const r = await fetch(`/repl/save/${gist.id}.json`, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
files: files.map((file) => ({
|
||||
name: `${file.name}.${file.type}`,
|
||||
source: file.source
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
if (r.status < 200 || r.status >= 300) {
|
||||
const { error } = await r.json();
|
||||
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
|
||||
}
|
||||
|
||||
modified_count = 0;
|
||||
repl.markSaved();
|
||||
justSaved = true;
|
||||
await wait(600);
|
||||
justSaved = false;
|
||||
} catch (err) {
|
||||
if (navigator.onLine) {
|
||||
alert(err.message);
|
||||
} else {
|
||||
alert(`It looks like you're offline! Find the internet and try again`);
|
||||
}
|
||||
}
|
||||
|
||||
saving = false;
|
||||
}
|
||||
|
||||
async function download() {
|
||||
downloading = true;
|
||||
|
||||
const { files: components, imports } = repl.toJSON();
|
||||
|
||||
const files = await (await fetch('/svelte-app.json')).json();
|
||||
|
||||
if (imports.length > 0) {
|
||||
const idx = files.findIndex(({ path }) => path === 'package.json');
|
||||
const pkg = JSON.parse(files[idx].data);
|
||||
const { devDependencies } = pkg;
|
||||
imports.forEach((mod) => {
|
||||
const match = /^(@[^/]+\/)?[^@/]+/.exec(mod);
|
||||
devDependencies[match[0]] = 'latest';
|
||||
});
|
||||
pkg.devDependencies = devDependencies;
|
||||
files[idx].data = JSON.stringify(pkg, null, ' ');
|
||||
}
|
||||
|
||||
files.push(
|
||||
...components.map((component) => ({
|
||||
path: `src/${component.name}.${component.type}`,
|
||||
data: component.source
|
||||
}))
|
||||
);
|
||||
files.push({
|
||||
path: `src/main.js`,
|
||||
data: `import App from './App.svelte';
|
||||
|
||||
var app = new App({
|
||||
target: document.body
|
||||
});
|
||||
|
||||
export default app;`
|
||||
});
|
||||
|
||||
downloadBlob(doNotZip.toBlob(files), 'svelte-app.zip');
|
||||
|
||||
downloading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<div class="app-controls">
|
||||
<input
|
||||
bind:value={name}
|
||||
on:focus={(e) => e.target.select()}
|
||||
use:enter={(e) => /** @type {HTMLInputElement} */ (e.target).blur()}
|
||||
/>
|
||||
|
||||
<div class="buttons">
|
||||
<button class="icon" on:click={() => (zen_mode = !zen_mode)} title="fullscreen editor">
|
||||
{#if zen_mode}
|
||||
<Icon name="close" />
|
||||
{:else}
|
||||
<Icon name="maximize" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button class="icon" disabled={downloading} on:click={download} title="download zip file">
|
||||
<Icon name="download" />
|
||||
</button>
|
||||
|
||||
<button class="icon" disabled={saving || !user} on:click={() => fork(false)} title="fork">
|
||||
{#if justForked}
|
||||
<Icon name="check" />
|
||||
{:else}
|
||||
<Icon name="git-branch" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button class="icon" disabled={saving || !user} on:click={save} title="save">
|
||||
{#if justSaved}
|
||||
<Icon name="check" />
|
||||
{:else}
|
||||
<Icon name="save" />
|
||||
{#if modified_count}
|
||||
<div class="badge">{modified_count}</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if user}
|
||||
<UserMenu {user} />
|
||||
{:else}
|
||||
<button class="icon" on:click|preventDefault={login}>
|
||||
<Icon name="log-in" />
|
||||
<span> Log in to save</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.app-controls {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: var(--app-controls-h);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.6rem var(--sk-page-padding-side);
|
||||
background-color: var(--sk-back-4);
|
||||
color: var(--sk-text-1);
|
||||
white-space: nowrap;
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
text-align: right;
|
||||
margin-right: 0.4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.2em;
|
||||
}
|
||||
|
||||
.icon {
|
||||
transform: translateY(0.1rem);
|
||||
display: inline-block;
|
||||
padding: 0.2em;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s;
|
||||
font-family: var(--sk-font);
|
||||
font-size: 1.6rem;
|
||||
color: var(--sk-text-1);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.icon:hover,
|
||||
.icon:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
.icon:disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.icon[title^='fullscreen'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: currentColor;
|
||||
font-family: var(--sk-font);
|
||||
font-size: 1.6rem;
|
||||
opacity: 0.7;
|
||||
outline: none;
|
||||
flex: 1;
|
||||
margin: 0 0.2em 0 0.4rem;
|
||||
padding-top: 0.2em;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
input:hover {
|
||||
border-bottom: 1px solid currentColor;
|
||||
opacity: 1;
|
||||
}
|
||||
input:focus {
|
||||
border-bottom: 1px solid currentColor;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
button span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: #ff3e00;
|
||||
border-radius: 100%;
|
||||
font-size: 10px;
|
||||
padding: 0;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
line-height: 15px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.icon[title^='fullscreen'] {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
button span {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,134 +0,0 @@
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
import { Icon } from '@sveltejs/site-kit/components';
|
||||
import { click_outside, focus_outside } from '@sveltejs/site-kit/actions';
|
||||
const { logout } = getContext('app');
|
||||
|
||||
export let user;
|
||||
|
||||
let showMenu = false;
|
||||
let name;
|
||||
|
||||
$: name = user.github_name || user.github_login;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="user"
|
||||
use:focus_outside={() => (showMenu = false)}
|
||||
use:click_outside={() => (showMenu = false)}
|
||||
>
|
||||
<button
|
||||
on:click={() => (showMenu = !showMenu)}
|
||||
aria-expanded={showMenu}
|
||||
class="trigger"
|
||||
aria-label={name}
|
||||
>
|
||||
<span class="name">{name}</span>
|
||||
<img alt="" src={user.github_avatar_url} />
|
||||
<Icon name={showMenu ? 'chevron-up' : 'chevron-down'} />
|
||||
</button>
|
||||
|
||||
{#if showMenu}
|
||||
<div class="menu">
|
||||
<a href="/apps">Your saved apps</a>
|
||||
<button on:click={logout}>Log out</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.user {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 0em 0 0 0.3rem;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
outline-offset: 2px;
|
||||
transform: translateY(0.1rem);
|
||||
--opacity: 0.7;
|
||||
}
|
||||
|
||||
.trigger:hover,
|
||||
.trigger:focus-visible,
|
||||
.trigger[aria-expanded='true'] {
|
||||
--opacity: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
line-height: 1;
|
||||
display: none;
|
||||
font-family: var(--sk-font);
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.name,
|
||||
.trigger :global(.icon) {
|
||||
display: none;
|
||||
opacity: var(--opacity);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 2.1rem;
|
||||
height: 2.1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 0.2rem;
|
||||
transform: translateY(-0.1rem);
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
width: calc(100% + 1.6rem);
|
||||
min-width: 10em;
|
||||
top: 3rem;
|
||||
right: -1.6rem;
|
||||
background-color: var(--sk-back-2);
|
||||
padding: 0.8rem 1.6rem;
|
||||
z-index: 99;
|
||||
text-align: left;
|
||||
border-radius: 0.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.menu button,
|
||||
.menu a {
|
||||
background-color: transparent;
|
||||
font-family: var(--sk-font);
|
||||
font-size: 1.6rem;
|
||||
opacity: 0.7;
|
||||
padding: 0.4rem 0;
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
border: none;
|
||||
color: var(--sk-text-2);
|
||||
}
|
||||
|
||||
.menu button:hover,
|
||||
.menu button:focus-visible,
|
||||
.menu a:hover,
|
||||
.menu a:focus-visible {
|
||||
opacity: 1;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.user {
|
||||
padding: 0em 0 0 1.6rem;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
.name,
|
||||
.trigger :global(.icon) {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,15 +0,0 @@
|
||||
/**
|
||||
* @param {Blob} blob
|
||||
* @param {string} filename
|
||||
*/
|
||||
export default (blob, filename) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
link.remove();
|
||||
};
|
@ -1,102 +0,0 @@
|
||||
import { dev } from '$app/environment';
|
||||
import { client } from '$lib/db/client.js';
|
||||
import * as gist from '$lib/db/gist.js';
|
||||
import examples_data from '$lib/generated/examples-data.js';
|
||||
import { get_example, get_examples_list } from '$lib/server/examples/index.js';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export const prerender = 'auto';
|
||||
|
||||
const UUID_REGEX = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/;
|
||||
|
||||
/** @type {Set<string>} */
|
||||
let examples;
|
||||
|
||||
/** @param {import('$lib/server/examples/types').ExamplesData[number]['examples'][number]['files'][number][]} files */
|
||||
function munge(files) {
|
||||
return files
|
||||
.map((file) => {
|
||||
const dot = file.name.lastIndexOf('.');
|
||||
let name = file.name.slice(0, dot);
|
||||
let type = file.name.slice(dot + 1);
|
||||
|
||||
if (type === 'html') type = 'svelte';
|
||||
// @ts-expect-error what is file.source? by @PuruVJ
|
||||
return { name, type, source: file.source ?? file.content ?? '' };
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.name === 'App' && a.type === 'svelte') return -1;
|
||||
if (b.name === 'App' && b.type === 'svelte') return 1;
|
||||
|
||||
if (a.type !== b.type) return a.type === 'svelte' ? -1 : 1;
|
||||
|
||||
return a.name < b.name ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET({ params }) {
|
||||
// Currently, these pages(that are in examples/) are prerendered. To avoid making any FS requests,
|
||||
// We prerender examples pages during build time. That means, when something like `/repl/hello-world.json`
|
||||
// is accessed, this function won't be run at all, as it will be served from the filesystem
|
||||
|
||||
examples = new Set(
|
||||
get_examples_list(examples_data)
|
||||
.map((category) => category.examples)
|
||||
.flat()
|
||||
.map((example) => example.slug)
|
||||
);
|
||||
|
||||
const example = get_example(examples_data, params.id);
|
||||
if (example) {
|
||||
return json({
|
||||
id: params.id,
|
||||
name: example.title,
|
||||
owner: null,
|
||||
relaxed: false, // TODO is this right? EDIT: It was example.relaxed before, which no example return to my knowledge. By @PuruVJ
|
||||
components: munge(example.files)
|
||||
});
|
||||
}
|
||||
|
||||
if (dev && !client) {
|
||||
// in dev with no local Supabase configured, proxy to production
|
||||
// this lets us at least load saved REPLs
|
||||
const res = await fetch(`https://svelte.dev/repl/api/${params.id}.json`);
|
||||
|
||||
// returning the response directly results in a bizarre
|
||||
// content encoding error, so we create a new one
|
||||
return new Response(await res.text(), {
|
||||
status: res.status,
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!UUID_REGEX.test(params.id)) {
|
||||
error(404);
|
||||
}
|
||||
|
||||
const app = await gist.read(params.id);
|
||||
|
||||
if (!app) {
|
||||
error(404, 'not found');
|
||||
}
|
||||
|
||||
return json({
|
||||
id: params.id,
|
||||
name: app.name,
|
||||
// @ts-ignore
|
||||
owner: app.userid,
|
||||
relaxed: false,
|
||||
// @ts-expect-error app.files has a `source` property
|
||||
components: munge(app.files)
|
||||
});
|
||||
}
|
||||
|
||||
export async function entries() {
|
||||
const { get_examples_list } = await import('$lib/server/examples/index.js');
|
||||
|
||||
return get_examples_list(examples_data)
|
||||
.map(({ examples }) => examples)
|
||||
.flatMap((val) => val.map(({ slug }) => ({ id: slug })));
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import * as gist from '$lib/db/gist';
|
||||
import * as session from '$lib/db/session';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const user = await session.from_cookie(request.headers.get('cookie'));
|
||||
if (!user) error(401);
|
||||
|
||||
const body = await request.json();
|
||||
const result = await gist.create(user, body);
|
||||
|
||||
// normalize id
|
||||
result.id = result.id.replace(/-/g, '');
|
||||
|
||||
return json(result, {
|
||||
status: 201
|
||||
});
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
export function load({ url }) {
|
||||
const query = url.searchParams;
|
||||
return {
|
||||
version: query.get('version') || '3',
|
||||
gist: query.get('gist'),
|
||||
example: query.get('example')
|
||||
};
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
<script>
|
||||
import { browser } from '$app/environment';
|
||||
import ReplWidget from '$lib/components/ReplWidget.svelte';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>REPL • Svelte</title>
|
||||
|
||||
<meta name="twitter:title" content="Svelte REPL" />
|
||||
<meta name="twitter:description" content="Cybernetically enhanced web apps" />
|
||||
<meta name="description" content="Interactive Svelte playground" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="repl-outer">
|
||||
{#if browser}
|
||||
<ReplWidget version={data.version} gist={data.gist} example={data.example} embedded={true} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.repl-outer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--sk-back-1);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
--pane-controls-h: 4.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
@ -1,15 +0,0 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
const local_svelte_path = env.LOCAL_SVELTE_PATH || '../../../svelte';
|
||||
|
||||
export async function GET({ params: { path } }) {
|
||||
if (import.meta.env.PROD || ('/' + path).includes('/.')) {
|
||||
return new Response(undefined, { status: 403 });
|
||||
}
|
||||
|
||||
const { readFile } = await import('node:fs/promises');
|
||||
|
||||
return new Response(await readFile(`${local_svelte_path}/${path}`), {
|
||||
headers: { 'Content-Type': 'text/javascript' }
|
||||
});
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import * as gist from '$lib/db/gist';
|
||||
import * as session from '$lib/db/session';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
// TODO reimplement as an action
|
||||
export async function PUT({ params, request }) {
|
||||
const user = await session.from_cookie(request.headers.get('cookie'));
|
||||
if (!user) error(401, 'Unauthorized');
|
||||
|
||||
const body = await request.json();
|
||||
await gist.update(user, params.id, body);
|
||||
|
||||
return new Response(undefined, { status: 204 });
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
|
||||
// we don't want to use <svelte:window bind:online> here,
|
||||
// because we only care about the online state when
|
||||
// the page first loads
|
||||
const online = typeof navigator !== 'undefined' ? navigator.onLine : true;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$page.status}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
{#if online}
|
||||
{#if $page.status === 404}
|
||||
<h1>Not found!</h1>
|
||||
<p>
|
||||
If you were expecting to find something here, please drop by the
|
||||
<a href="/chat"> Discord chatroom </a>
|
||||
and let us know, or raise an issue on
|
||||
<a href="https://github.com/sveltejs/sites">GitHub</a>. Thanks!
|
||||
</p>
|
||||
{:else}
|
||||
<h1>Yikes!</h1>
|
||||
<p>Something went wrong when we tried to render this page.</p>
|
||||
{#if $page.error.message}
|
||||
<p class="error">{$page.status}: {$page.error.message}</p>
|
||||
{:else}
|
||||
<p class="error">Encountered a {$page.status} error.</p>
|
||||
{/if}
|
||||
<p>Please try reloading the page.</p>
|
||||
<p>
|
||||
If the error persists, please drop by the
|
||||
<a href="/chat"> Discord chatroom </a>
|
||||
and let us know, or raise an issue on
|
||||
<a href="https://github.com/sveltejs/sites">GitHub</a>. Thanks!
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<h1>It looks like you're offline</h1>
|
||||
<p>Reload the page once you've found the internet.</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
padding: var(--sk-page-padding-top) var(--sk-page-padding-side) 6rem var(--sk-page-padding-side);
|
||||
}
|
||||
|
||||
h1,
|
||||
p {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.8em;
|
||||
font-weight: 300;
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: var(--sk-theme-2);
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
font: 600 16px/1.7 var(--sk-font);
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
@ -1,29 +0,0 @@
|
||||
export const load = async ({ url, fetch }) => {
|
||||
const nav_list = await fetch('/nav.json').then((r) => r.json());
|
||||
|
||||
return {
|
||||
nav_title: get_nav_title(url),
|
||||
nav_links: nav_list
|
||||
};
|
||||
};
|
||||
|
||||
/** @param {URL} url */
|
||||
function get_nav_title(url) {
|
||||
const list = new Map([
|
||||
[/^docs/, 'Docs'],
|
||||
[/^repl/, 'REPL'],
|
||||
[/^blog/, 'Blog'],
|
||||
[/^faq/, 'FAQ'],
|
||||
[/^tutorial/, 'Tutorial'],
|
||||
[/^search/, 'Search'],
|
||||
[/^examples/, 'Examples']
|
||||
]);
|
||||
|
||||
for (const [regex, title] of list) {
|
||||
if (regex.test(url.pathname.replace(/^\/(.+)/, '$1'))) {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
<script>
|
||||
import { browser } from '$app/environment';
|
||||
import { page } from '$app/stores';
|
||||
import { Icon, Shell } from '@sveltejs/site-kit/components';
|
||||
import { Nav, Separator } from '@sveltejs/site-kit/nav';
|
||||
import { Search, SearchBox } from '@sveltejs/site-kit/search';
|
||||
import '@sveltejs/site-kit/styles/index.css';
|
||||
|
||||
export let data;
|
||||
|
||||
/** @type {import('@sveltejs/kit').Snapshot<number>} */
|
||||
let shell_snapshot;
|
||||
|
||||
export const snapshot = {
|
||||
capture() {
|
||||
return {
|
||||
shell: shell_snapshot?.capture()
|
||||
};
|
||||
},
|
||||
restore(data) {
|
||||
shell_snapshot?.restore(data.shell);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if !$page.route.id?.startsWith('/blog/')}
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:image" content="https://svelte.dev/images/twitter-thumbnail.jpg" />
|
||||
<meta name="og:image" content="https://svelte.dev/images/twitter-thumbnail.jpg" />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<div style:display={$page.url.pathname !== '/docs' ? 'contents' : 'none'}>
|
||||
<Shell nav_visible={$page.url.pathname !== '/repl/embed'} bind:snapshot={shell_snapshot}>
|
||||
<Nav slot="top-nav" title={data.nav_title} links={data.nav_links}>
|
||||
<svelte:fragment slot="home-large">
|
||||
<strong>svelte</strong>.dev
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="home-small">
|
||||
<strong>svelte</strong>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="search">
|
||||
{#if $page.url.pathname !== '/search'}
|
||||
<Search />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="external-links">
|
||||
<a href="https://learn.svelte.dev/">Tutorial</a>
|
||||
|
||||
<a href="https://kit.svelte.dev">SvelteKit</a>
|
||||
|
||||
<Separator />
|
||||
|
||||
<a href="/chat" title="Discord Chat">
|
||||
<span class="small">Discord</span>
|
||||
<span class="large"><Icon name="discord" /></span>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/sveltejs/svelte" title="GitHub Repo">
|
||||
<span class="small">GitHub</span>
|
||||
<span class="large"><Icon name="github" /></span>
|
||||
</a>
|
||||
</svelte:fragment>
|
||||
</Nav>
|
||||
|
||||
<slot />
|
||||
</Shell>
|
||||
</div>
|
||||
|
||||
{#if browser}
|
||||
<SearchBox />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
:global(:root) {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
:global(html, body) {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@ -1 +0,0 @@
|
||||
export const prerender = true;
|
@ -1,109 +0,0 @@
|
||||
<script>
|
||||
import { Blurb, Footer, TrySection } from '@sveltejs/site-kit/home';
|
||||
import Demo from './_components/Demo.svelte';
|
||||
import Hero from './_components/Hero.svelte';
|
||||
import Supporters from './_components/Supporters/index.svelte';
|
||||
import WhosUsingSvelte from './_components/WhosUsingSvelte/index.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Svelte • Cybernetically enhanced web apps</title>
|
||||
|
||||
<meta name="twitter:title" content="Svelte" />
|
||||
<meta name="twitter:description" content="Cybernetically enhanced web apps" />
|
||||
<meta name="description" content="Cybernetically enhanced web apps" />
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="visually-hidden">Svelte</h1>
|
||||
|
||||
<Hero />
|
||||
|
||||
<Blurb --background="var(--sk-back-1)">
|
||||
<div slot="one">
|
||||
<h2>compiled</h2>
|
||||
<p>
|
||||
Svelte shifts as much work as possible out of the browser and into your build step. No more
|
||||
manual optimisations — just faster, more efficient apps.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div slot="two">
|
||||
<h2>compact</h2>
|
||||
<p>
|
||||
Write breathtakingly concise components using languages you already know — HTML, CSS and
|
||||
JavaScript. Oh, and your application bundles will be tiny as well.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div slot="three">
|
||||
<h2>complete</h2>
|
||||
<p>
|
||||
Built-in scoped styling, state management, motion primitives, form bindings and more — don't
|
||||
waste time trawling npm for the bare essentials. It's all here.
|
||||
</p>
|
||||
</div>
|
||||
</Blurb>
|
||||
|
||||
<TrySection />
|
||||
|
||||
<Demo />
|
||||
|
||||
<WhosUsingSvelte />
|
||||
|
||||
<Supporters />
|
||||
|
||||
<Footer
|
||||
links={{
|
||||
resources: [
|
||||
{
|
||||
title: 'documentation',
|
||||
href: '/docs'
|
||||
},
|
||||
{
|
||||
title: 'tutorial',
|
||||
href: '/tutorial'
|
||||
},
|
||||
{
|
||||
title: 'examples',
|
||||
href: '/examples'
|
||||
},
|
||||
{
|
||||
title: 'blog',
|
||||
href: '/blog'
|
||||
}
|
||||
],
|
||||
connect: [
|
||||
{
|
||||
title: 'github',
|
||||
href: 'https://github.com/sveltejs/svelte'
|
||||
},
|
||||
{
|
||||
title: 'opencollective',
|
||||
href: 'https://opencollective.com/svelte'
|
||||
},
|
||||
{
|
||||
title: 'discord',
|
||||
href: '/chat'
|
||||
},
|
||||
{
|
||||
title: 'twitter',
|
||||
href: 'https://twitter.com/sveltejs'
|
||||
}
|
||||
]
|
||||
}}
|
||||
>
|
||||
<span slot="license">
|
||||
Svelte is <a href="https://github.com/sveltejs/svelte">free and open source software</a> released
|
||||
under the MIT license
|
||||
</span>
|
||||
</Footer>
|
||||
|
||||
<style>
|
||||
h2 {
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--sk-text-m);
|
||||
}
|
||||
</style>
|
@ -1,181 +0,0 @@
|
||||
<script>
|
||||
import Example from './Example.svelte';
|
||||
import { Section } from '@sveltejs/site-kit/components';
|
||||
|
||||
const examples = [
|
||||
{
|
||||
id: 'hello-world',
|
||||
title: 'Hello World',
|
||||
description: 'Svelte components are built on top of HTML. Just add data.'
|
||||
},
|
||||
{
|
||||
id: 'nested-components',
|
||||
title: 'Scoped CSS',
|
||||
description:
|
||||
'CSS is component-scoped by default — no more style collisions or specificity wars. Or you can <a href="/blog/svelte-css-in-js">use your favourite CSS-in-JS library</a >.'
|
||||
},
|
||||
{
|
||||
id: 'reactive-assignments',
|
||||
title: 'Reactivity',
|
||||
description:
|
||||
'Trigger efficient, granular updates by assigning to local variables. The compiler does the rest.'
|
||||
},
|
||||
{
|
||||
id: 'svg-transitions',
|
||||
title: 'Transitions',
|
||||
description:
|
||||
'Build beautiful UIs with a powerful, performant transition engine built right into the framework.'
|
||||
}
|
||||
];
|
||||
|
||||
let selected = examples[0];
|
||||
</script>
|
||||
|
||||
<Section --background="var(--sk-back-2)">
|
||||
<h3>build with ease</h3>
|
||||
|
||||
<div class="container">
|
||||
<div class="controls">
|
||||
<div class="tabs">
|
||||
{#each examples as example, i}
|
||||
<button
|
||||
class="tab"
|
||||
class:selected={selected === example}
|
||||
on:click={() => (selected = example)}
|
||||
>
|
||||
<span class="small-show">{i + 1}</span>
|
||||
<span class="small-hide">{example.title}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<a href="/examples">more <span class="large-show"> examples</span> →</a>
|
||||
</div>
|
||||
|
||||
{#if selected}
|
||||
<Example id={selected?.id} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p class="description">{@html selected?.description}</p>
|
||||
</Section>
|
||||
|
||||
<style>
|
||||
h3 {
|
||||
font-size: var(--sk-text-xl);
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--sk-text-2);
|
||||
}
|
||||
|
||||
.container {
|
||||
filter: drop-shadow(6px 10px 20px rgba(0, 0, 0, 0.2));
|
||||
margin: 4rem 0;
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: relative;
|
||||
top: 4px;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 5rem;
|
||||
grid-template-columns: 4fr 1fr;
|
||||
color: var(--sk-text-1);
|
||||
align-items: center;
|
||||
font-size: var(--sk-text-s);
|
||||
}
|
||||
|
||||
a {
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
height: 100%;
|
||||
background-color: var(--sk-back-1);
|
||||
border-radius: var(--sk-border-radius);
|
||||
}
|
||||
|
||||
button,
|
||||
a {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 0.5px solid var(--sk-text-4);
|
||||
border-right: 0.5px solid color-mix(in hsl, var(--sk-text-4), transparent 40%);
|
||||
background-color: var(--sk-back-4);
|
||||
transition: 0.15s ease;
|
||||
transition-property: transform, background-color, color;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--sk-back-3);
|
||||
background-color: color-mix(in srgb, var(--sk-back-4) 70%, var(--sk-back-1) 30%);
|
||||
}
|
||||
|
||||
button:has(+ .selected) {
|
||||
border-right: initial;
|
||||
}
|
||||
|
||||
button:first-child {
|
||||
border-radius: var(--sk-border-radius) 0 0 0;
|
||||
}
|
||||
button:last-child {
|
||||
border-radius: 0 var(--sk-border-radius) 0 0;
|
||||
border-right: initial;
|
||||
}
|
||||
|
||||
button.selected {
|
||||
background-color: var(--sk-back-1);
|
||||
color: var(--sk-text-2);
|
||||
border-radius: var(--sk-border-radius) var(--sk-border-radius) 0 0;
|
||||
border-right: initial;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
a {
|
||||
border-right: initial;
|
||||
border-radius: 0 var(--sk-border-radius) var(--sk-border-radius) 0;
|
||||
background-color: initial;
|
||||
}
|
||||
|
||||
.small-show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.small-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.large-show {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.description :global(a) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.small-show {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.small-hide {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.controls {
|
||||
font-size: var(--sk-text-s);
|
||||
}
|
||||
|
||||
.large-show {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,23 +0,0 @@
|
||||
<script>
|
||||
import IntersectionObserver from '$lib/components/IntersectionObserver.svelte';
|
||||
import ReplWidget from '$lib/components/ReplWidget.svelte';
|
||||
|
||||
export let id;
|
||||
</script>
|
||||
|
||||
<div class="repl-container">
|
||||
<IntersectionObserver once let:intersecting top={400}>
|
||||
{#if intersecting}
|
||||
<ReplWidget example={id} />
|
||||
{/if}
|
||||
</IntersectionObserver>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.repl-container {
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
border-radius: var(--sk-border-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
@ -1,182 +0,0 @@
|
||||
<script>
|
||||
import { Icon } from '@sveltejs/site-kit/components';
|
||||
import SvelteLogotype from './svelte-logotype.svg';
|
||||
// @ts-ignore
|
||||
import MachineDesktop from './svelte-machine-desktop.png?w=1200;2000;2800;4400&format=avif;webp;png;&as=picture';
|
||||
// @ts-ignore
|
||||
import MachineMobile from './svelte-machine-mobile.png?w=960&format=avif;webp;png;&as=picture';
|
||||
|
||||
const srcset = (sources) => sources.map(({ src, w }) => `${src} ${w}w`).join(', ');
|
||||
</script>
|
||||
|
||||
<div class="hero">
|
||||
<div class="hero-content">
|
||||
<img alt="Svelte logotype" class="logotype" src={SvelteLogotype} width="300" height="56" />
|
||||
<strong>
|
||||
<span style="white-space: nowrap">Cybernetically enhanced</span> <br /> web apps
|
||||
</strong>
|
||||
<div class="buttons">
|
||||
<a href="https://learn.svelte.dev" rel="external" class="cta">
|
||||
tutorial<Icon name="external-link" size="1em" />
|
||||
</a>
|
||||
<a href="/docs/introduction" class="cta basic">read the docs</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<picture class="machine">
|
||||
<source
|
||||
srcset={srcset(MachineDesktop.sources.avif)}
|
||||
type="image/avif"
|
||||
media="(min-width: 800px)"
|
||||
/>
|
||||
<source
|
||||
srcset={srcset(MachineDesktop.sources.webp)}
|
||||
type="image/webp"
|
||||
media="(min-width: 800px)"
|
||||
/>
|
||||
<source
|
||||
srcset={srcset(MachineDesktop.sources.png)}
|
||||
type="image/png"
|
||||
media="(min-width: 800px)"
|
||||
/>
|
||||
<source srcset={srcset(MachineMobile.sources.avif)} type="image/avif" />
|
||||
<source srcset={srcset(MachineMobile.sources.webp)} type="image/webp" />
|
||||
<source srcset={srcset(MachineMobile.sources.png)} type="image/png" />
|
||||
<img alt="The Svelte compiler packaging up your component code" src={MachineMobile.img.src} />
|
||||
</picture>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
position: relative;
|
||||
background: radial-gradient(circle at 40% 30%, rgb(235, 243, 249), rgb(214, 222, 228));
|
||||
padding: 6rem 0 34vw 0;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.machine img {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: var(--sk-text-l);
|
||||
text-align: center;
|
||||
font-family: var(--sk-font);
|
||||
text-transform: lowercase;
|
||||
font-weight: 400;
|
||||
color: var(--sk-text-2);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.1rem;
|
||||
background: var(--sk-theme-1);
|
||||
padding: 0.35em 0.8em;
|
||||
font-size: var(--sk-text-s);
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
border-radius: var(--sk-border-radius);
|
||||
box-shadow: 0px 6px 14px rgba(0, 0, 0, 0.08);
|
||||
color: #fff;
|
||||
transition: 0.5s var(--quint-out);
|
||||
transition-property: box-shadow, color;
|
||||
}
|
||||
|
||||
.cta:hover {
|
||||
text-decoration: none;
|
||||
box-shadow:
|
||||
0px 0.8px 3.8px rgba(0, 0, 0, 0.115),
|
||||
0px 6px 30px rgba(0, 0, 0, 0.23);
|
||||
}
|
||||
|
||||
.cta.basic {
|
||||
background-color: var(--sk-back-5);
|
||||
|
||||
color: var(--sk-text-1);
|
||||
}
|
||||
|
||||
.logotype {
|
||||
width: min(45vw, 40em);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.hero-content {
|
||||
--width: clamp(60rem, 50vw, 80rem);
|
||||
position: absolute;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-column-gap: 4rem;
|
||||
grid-row-gap: 2rem;
|
||||
width: var(--width);
|
||||
left: calc(0.5 * (100% - min(100vw, 120rem)) + var(--sk-page-padding-side));
|
||||
top: 6rem;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.logotype {
|
||||
width: 100%;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
strong {
|
||||
text-align: left;
|
||||
font-size: calc(0.04 * var(--width));
|
||||
}
|
||||
|
||||
.hero {
|
||||
height: calc(14rem + 20vw);
|
||||
padding: 14rem 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.hero-content {
|
||||
grid-template-columns: 1fr;
|
||||
width: calc(0.5 * var(--width));
|
||||
top: 6vw;
|
||||
}
|
||||
|
||||
.hero {
|
||||
height: calc(10rem + 20vw);
|
||||
padding: 10rem 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
:global(body.dark) .hero {
|
||||
background: radial-gradient(
|
||||
64.14% 72.25% at 47.58% 31.75%,
|
||||
hsl(209deg 6% 47% / 52%) 0%,
|
||||
hsla(0, 0%, 100%, 0) 100%
|
||||
),
|
||||
linear-gradient(
|
||||
92.4deg,
|
||||
hsl(210, 7%, 16%) 14.67%,
|
||||
hsl(0deg 0% 0% / 48%) 54.37%,
|
||||
hsla(207, 22%, 13%, 0.62) 92.49%
|
||||
),
|
||||
linear-gradient(0deg, hsl(204, 38%, 20%), hsl(204, 10%, 90%));
|
||||
}
|
||||
|
||||
:global(body.dark) .logotype {
|
||||
filter: invert(4) brightness(1.2);
|
||||
}
|
||||
</style>
|
@ -1,31 +0,0 @@
|
||||
<script>
|
||||
/** @type {ImageToolsPictureData} */
|
||||
export let src;
|
||||
|
||||
/** @type {string} */
|
||||
export let alt;
|
||||
|
||||
export let lazy = false;
|
||||
</script>
|
||||
|
||||
<picture>
|
||||
{#each Object.entries(src.sources) as [format, images]}
|
||||
<source srcset={images.map((i) => `${i.src} ${i.w}w`).join(', ')} type="image/{format}" />
|
||||
{/each}
|
||||
<img
|
||||
loading={lazy ? 'lazy' : 'eager'}
|
||||
height={src.img.h}
|
||||
width={src.img.w}
|
||||
src={src.img.src}
|
||||
{alt}
|
||||
/>
|
||||
</picture>
|
||||
|
||||
<style>
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
@ -1,151 +0,0 @@
|
||||
<script>
|
||||
import { Section } from '@sveltejs/site-kit/components';
|
||||
import contributors from './contributors.js';
|
||||
import donors from './donors.js';
|
||||
|
||||
// @ts-ignore
|
||||
import contributors_img from './contributors.jpg?w=1200&format=webp';
|
||||
// @ts-ignore
|
||||
import donors_img from './donors.jpg?w=1200&format=webp';
|
||||
</script>
|
||||
|
||||
<Section --background="var(--sk-back-2">
|
||||
<p class="intro">Svelte is made possible by the work of hundreds of supporters.</p>
|
||||
|
||||
<div class="layout">
|
||||
<div class="contributors blurb">
|
||||
<h3>contributors</h3>
|
||||
<p>
|
||||
<a href="https://github.com/sveltejs/svelte/graphs/contributors">Join us on GitHub</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="contributors grid">
|
||||
{#each contributors as contributor, i}
|
||||
<a
|
||||
class="supporter"
|
||||
style="background-position: {(100 * i) / (contributors.length - 1)}% 0"
|
||||
style:background-image="url({contributors_img})"
|
||||
href="https://github.com/{contributor}"
|
||||
>
|
||||
{contributor}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="donors blurb">
|
||||
<h3>donors</h3>
|
||||
<p><a href="https://opencollective.com/svelte">Support us on OpenCollective</a></p>
|
||||
</div>
|
||||
|
||||
<div class="donors grid">
|
||||
{#each donors as donor, i}
|
||||
<a
|
||||
class="supporter"
|
||||
style="background-position: {(100 * i) / (donors.length - 1)}% 0"
|
||||
style:background-image="url({donors_img})"
|
||||
href="https://opencollective.com/svelte">{donor}</a
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<style>
|
||||
h3 {
|
||||
color: var(--sk-text-2);
|
||||
font-size: var(--sk-text-l);
|
||||
}
|
||||
|
||||
.intro {
|
||||
max-width: 28em; /* text balancing */
|
||||
margin: 0 0 3.2rem 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
grid-gap: 1em;
|
||||
}
|
||||
|
||||
.contributors.grid {
|
||||
margin: 0 0 2em 0;
|
||||
}
|
||||
|
||||
a[href] {
|
||||
color: color-mix(in srgb, var(--sk-theme-1) 90%, var(--sk-text-1) 15%);
|
||||
}
|
||||
|
||||
.supporter {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
text-indent: -9999px;
|
||||
display: inline-block;
|
||||
background: no-repeat;
|
||||
background-size: auto 102%;
|
||||
filter: grayscale(1) opacity(0.7) drop-shadow(2px 4px 6px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
.supporter:hover {
|
||||
filter: drop-shadow(1px 2px 8px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 32rem 2fr;
|
||||
grid-row-gap: 1em;
|
||||
}
|
||||
|
||||
.intro {
|
||||
font-size: var(--sk-text-m);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
grid-gap: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 880px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1100px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.intro {
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,110 +0,0 @@
|
||||
export const companies = [
|
||||
{
|
||||
href: 'https://1password.com',
|
||||
filename: '1password.svg',
|
||||
alt: '1Password logo',
|
||||
width: 364,
|
||||
height: 68
|
||||
},
|
||||
{
|
||||
href: 'https://www.alaskaair.com/',
|
||||
// style: 'background-color: black',
|
||||
filename: 'alaskaairlines.svg',
|
||||
alt: 'Alaska Airlines logo',
|
||||
// invert: true,
|
||||
width: 113,
|
||||
height: 48
|
||||
},
|
||||
{
|
||||
href: 'https://avast.com',
|
||||
filename: 'avast.svg',
|
||||
alt: 'Avast logo',
|
||||
width: 300,
|
||||
height: 95
|
||||
},
|
||||
{
|
||||
href: 'https://chess.com',
|
||||
filename: 'chess.svg',
|
||||
alt: 'Chess.com logo',
|
||||
|
||||
width: 300,
|
||||
height: 85
|
||||
},
|
||||
{
|
||||
href: 'https://fusioncharts.com',
|
||||
filename: 'fusioncharts.svg',
|
||||
alt: 'FusionCharts logo',
|
||||
width: 735,
|
||||
height: 115
|
||||
},
|
||||
{
|
||||
href: 'https://godaddy.com',
|
||||
filename: 'godaddy.svg',
|
||||
alt: 'GoDaddy logo',
|
||||
width: 300,
|
||||
height: 84
|
||||
},
|
||||
{
|
||||
href: 'https://www.ibm.com/',
|
||||
filename: 'ibm.svg',
|
||||
alt: 'IBM logo',
|
||||
width: 1000,
|
||||
height: 400
|
||||
},
|
||||
{
|
||||
href: 'https://media.lesechos.fr/infographie',
|
||||
filename: 'les-echos.svg',
|
||||
alt: 'Les Echos',
|
||||
width: 142,
|
||||
height: 33
|
||||
},
|
||||
{
|
||||
href: 'https://www.philips.co.uk',
|
||||
filename: 'philips.svg',
|
||||
alt: 'Philips logo',
|
||||
width: 140,
|
||||
height: 30
|
||||
},
|
||||
{
|
||||
href: 'https://global.rakuten.com/corp/',
|
||||
filename: 'rakuten.svg',
|
||||
alt: 'Rakuten logo',
|
||||
width: 300,
|
||||
height: 89
|
||||
},
|
||||
{
|
||||
href: 'https://razorpay.com',
|
||||
filename: 'razorpay.svg',
|
||||
alt: 'Razorpay logo',
|
||||
width: 316,
|
||||
height: 67
|
||||
},
|
||||
// {
|
||||
// href: 'https://www.se.com',
|
||||
// style: ' background-color: black',
|
||||
// filename: 'Schneider_Electric.svg',
|
||||
// alt: 'Schneider Electric',
|
||||
// invert: true
|
||||
// },
|
||||
{
|
||||
href: 'https://squareup.com',
|
||||
filename: 'square.svg',
|
||||
alt: 'Square',
|
||||
width: 144,
|
||||
height: 36
|
||||
},
|
||||
{
|
||||
href: 'https://nytimes.com',
|
||||
filename: 'nyt.svg',
|
||||
alt: 'The New York Times logo',
|
||||
width: 300,
|
||||
height: 49
|
||||
},
|
||||
{
|
||||
href: 'https://transloadit.com',
|
||||
filename: 'transloadit.svg',
|
||||
alt: 'Transloadit',
|
||||
width: 239,
|
||||
height: 60
|
||||
}
|
||||
];
|
@ -1,117 +0,0 @@
|
||||
<script>
|
||||
import { Section } from '@sveltejs/site-kit/components';
|
||||
import { theme } from '@sveltejs/site-kit/stores';
|
||||
import { companies } from './companies.js';
|
||||
|
||||
const sorted = companies.sort((a, b) => (a.alt < b.alt ? -1 : 1));
|
||||
</script>
|
||||
|
||||
<Section --background={$theme.current === 'light' ? 'var(--sk-back-4)' : '#222'}>
|
||||
<h3>loved by developers</h3>
|
||||
|
||||
<p>
|
||||
We're proud that Svelte was recently voted the <a
|
||||
href="https://survey.stackoverflow.co/2023/#section-admired-and-desired-web-frameworks-and-technologies"
|
||||
>most admired JS web framework</a
|
||||
>
|
||||
in one industry survey while drawing the most interest in learning it in
|
||||
<a
|
||||
href="https://tsh.io/state-of-frontend/#which-of-the-following-frameworks-would-you-like-to-learn-in-the-future"
|
||||
>two</a
|
||||
> <a href="https://2022.stateofjs.com/en-US/libraries/front-end-frameworks/">others</a>. We
|
||||
think you'll love it too.
|
||||
</p>
|
||||
|
||||
<section class="whos-using-svelte-container" class:dark={$theme.current === 'dark'}>
|
||||
<div class="logos">
|
||||
{#each sorted as { href, filename, alt, style, invert, width, height }}
|
||||
<a target="_blank" rel="noreferrer" {href} class:invert style={style || ''}>
|
||||
<img src="/whos-using-svelte/{filename}" {alt} {width} {height} loading="lazy" />
|
||||
</a>
|
||||
|
||||
<span class="spacer" />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</Section>
|
||||
|
||||
<style>
|
||||
h3 {
|
||||
font-size: var(--sk-text-xl);
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 28em; /* text balancing */
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
p {
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.logos {
|
||||
display: flex;
|
||||
margin: 6rem 0 0 0;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 1em;
|
||||
justify-content: center;
|
||||
--row-size: 3;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: calc(100% / calc(2 * var(--row-size) - 1));
|
||||
}
|
||||
|
||||
.logos a {
|
||||
width: calc(100% / calc(2 * var(--row-size) - 1));
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
color: var(--sk-text-2);
|
||||
filter: grayscale(1) contrast(4) opacity(0.4) invert(var(--invert, 0));
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.logos a:last-of-type {
|
||||
/* hide last item at this screen size, it ruins wrapping */
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logos a.invert {
|
||||
--invert: 1;
|
||||
}
|
||||
|
||||
img {
|
||||
padding: 5px 10px;
|
||||
transition: transform 0.2s;
|
||||
min-width: 0; /* Avoid image overflow in Safari */
|
||||
width: 100%;
|
||||
height: auto;
|
||||
/* mix-blend-mode: multiply; */
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.logos {
|
||||
--row-size: 4;
|
||||
}
|
||||
|
||||
.logos a:last-of-type {
|
||||
/* show 14 items instead of 13 — wraps better */
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.logos {
|
||||
--row-size: 5;
|
||||
}
|
||||
}
|
||||
|
||||
:global(body.dark) .logos a {
|
||||
--invert: 1;
|
||||
filter: grayscale(1) contrast(4) opacity(0.7) invert(var(--invert, 0));
|
||||
}
|
||||
</style>
|
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 994 KiB |
Before Width: | Height: | Size: 1.4 MiB |
@ -1,6 +0,0 @@
|
||||
import { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } from '$env/static/private';
|
||||
|
||||
export const oauth = 'https://github.com/login/oauth';
|
||||
|
||||
export const client_id = GITHUB_CLIENT_ID;
|
||||
export const client_secret = GITHUB_CLIENT_SECRET;
|
@ -1,63 +0,0 @@
|
||||
import { uneval } from 'devalue';
|
||||
import * as cookie from 'cookie';
|
||||
import * as session from '$lib/db/session';
|
||||
import { oauth, client_id, client_secret } from '../_config.js';
|
||||
|
||||
export async function GET({ url }) {
|
||||
try {
|
||||
// Trade "code" for "access_token"
|
||||
const code = url.searchParams.get('code') || undefined;
|
||||
const params = new URLSearchParams({
|
||||
client_id,
|
||||
client_secret
|
||||
});
|
||||
if (code) params.set('code', code);
|
||||
const r1 = await fetch(`${oauth}/access_token?` + params.toString());
|
||||
const access_token = new URLSearchParams(await r1.text()).get('access_token');
|
||||
|
||||
// Now fetch User details
|
||||
const r2 = await fetch('https://api.github.com/user', {
|
||||
headers: {
|
||||
'User-Agent': 'svelte.dev',
|
||||
Authorization: `token ${access_token}`
|
||||
}
|
||||
});
|
||||
|
||||
const profile = await r2.json();
|
||||
|
||||
// Create or update user in database, and create a session
|
||||
|
||||
const user = {
|
||||
github_id: profile.id,
|
||||
github_name: profile.name,
|
||||
github_login: profile.login,
|
||||
github_avatar_url: profile.avatar_url
|
||||
};
|
||||
|
||||
const { sessionid, expires } = await session.create(user);
|
||||
|
||||
return new Response(
|
||||
`
|
||||
<script>
|
||||
window.opener.postMessage({
|
||||
user: ${uneval(user)}
|
||||
}, window.location.origin);
|
||||
</script>
|
||||
`,
|
||||
{
|
||||
headers: {
|
||||
'Set-Cookie': cookie.serialize('sid', sessionid, {
|
||||
expires: new Date(expires),
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: url.protocol === 'https'
|
||||
}),
|
||||
'Content-Type': 'text/html; charset=utf-8'
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('GET /auth/callback', err);
|
||||
return new Response(err.data, { status: 500 });
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { client_id, oauth } from '../_config.js';
|
||||
|
||||
export const GET = client_id
|
||||
? /** @param {{url: URL}} opts */ ({ url }) => {
|
||||
const Location =
|
||||
`${oauth}/authorize?` +
|
||||
new URLSearchParams({
|
||||
scope: 'read:user',
|
||||
client_id,
|
||||
redirect_uri: `${url.origin}/auth/callback`
|
||||
}).toString();
|
||||
|
||||
redirect(302, Location);
|
||||
}
|
||||
: () =>
|
||||
new Response(
|
||||
`
|
||||
<body style="font-family: sans-serif; background: rgb(255,215,215); border: 2px solid red; margin: 0; padding: 1em;">
|
||||
<h1>Missing .env file</h1>
|
||||
<p>In order to use GitHub authentication, you will need to <a target="_blank" href="https://github.com/settings/developers">register an OAuth application</a> and create a local .env file:</p>
|
||||
<pre>GITHUB_CLIENT_ID=[YOUR_APP_ID]\nGITHUB_CLIENT_SECRET=[YOUR_APP_SECRET]\nBASEURL=http://localhost:5173</pre>
|
||||
<p>The <code>BASEURL</code> variable should match the callback URL specified for your app.</p>
|
||||
<p>See also <a target="_blank" href="https://github.com/sveltejs/svelte/tree/master/site#repl-github-integration">here</a></p>
|
||||
</body>
|
||||
`,
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8'
|
||||
}
|
||||
}
|
||||
);
|
@ -1,18 +0,0 @@
|
||||
import * as cookie from 'cookie';
|
||||
import * as session from '$lib/db/session';
|
||||
|
||||
export async function GET({ request, url }) {
|
||||
const cookies = cookie.parse(request.headers.get('cookie') || '');
|
||||
await session.destroy(cookies.sid);
|
||||
|
||||
return new Response(undefined, {
|
||||
headers: {
|
||||
'Set-Cookie': cookie.serialize('sid', '', {
|
||||
maxAge: -1,
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: url.protocol === 'https'
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { get_blog_data, get_blog_list } from '$lib/server/blog/index.js';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function load() {
|
||||
return {
|
||||
posts: get_blog_list(await get_blog_data())
|
||||
};
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import { get_blog_data, get_processed_blog_post } from '$lib/server/blog/index.js';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function load({ params }) {
|
||||
const post = await get_processed_blog_post(await get_blog_data(), params.slug);
|
||||
|
||||
if (!post) error(404);
|
||||
|
||||
// forgive me — terrible hack necessary to get diffs looking sensible
|
||||
// on the `runes` blog post
|
||||
post.content = post.content.replace(/( )+/gm, (match) => ' '.repeat(match.length / 4));
|
||||
|
||||
return {
|
||||
post
|
||||
};
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { copy_code_descendants } from '@sveltejs/site-kit/actions';
|
||||
import { DocsOnThisPage, setupDocsHovers } from '@sveltejs/site-kit/docs';
|
||||
|
||||
export let data;
|
||||
|
||||
setupDocsHovers();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.post.title}</title>
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={data.post.title} />
|
||||
<meta name="twitter:description" content={data.post.description} />
|
||||
<meta name="description" content={data.post.description} />
|
||||
|
||||
<meta name="twitter:image" content="https://svelte.dev/blog/{$page.params.slug}/card.png" />
|
||||
<meta name="og:image" content="https://svelte.dev/blog/{$page.params.slug}/card.png" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="content">
|
||||
<article class="post listify text" use:copy_code_descendants>
|
||||
<h1>{data.post.title}</h1>
|
||||
<p class="standfirst">{data.post.description}</p>
|
||||
|
||||
<p class="byline">
|
||||
<a href={data.post.author.url}>{data.post.author.name}</a>
|
||||
<time datetime={data.post.date}>{data.post.date_formatted}</time>
|
||||
</p>
|
||||
|
||||
<DocsOnThisPage
|
||||
details={{
|
||||
content: '',
|
||||
file: '',
|
||||
path: `/blog/${data.post.slug}`,
|
||||
sections: data.post.sections,
|
||||
slug: data.post.slug,
|
||||
title: data.post.title
|
||||
}}
|
||||
orientation="inline"
|
||||
/>
|
||||
|
||||
{@html data.post.content}
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- the crawler doesn't understand twitter:image etc, so we have to add this hack. TODO fix in sveltekit -->
|
||||
<img hidden src="/blog/{$page.params.slug}/card.png" alt="Social card for {data.post.title}" />
|
||||
|
||||
<style>
|
||||
.post {
|
||||
padding: var(--sk-page-padding-top) var(--sk-page-padding-side) 6rem var(--sk-page-padding-side);
|
||||
max-width: var(--sk-page-main-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.standfirst {
|
||||
font-size: var(--sk-text-s);
|
||||
color: var(--sk-text-3);
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
.byline {
|
||||
margin: 0 0 1rem 0;
|
||||
padding: 1.6rem 0 0 0;
|
||||
border-top: var(--sk-thick-border-width) solid #6767785b;
|
||||
font-size: var(--sk-text-xs);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.post :global(figure) {
|
||||
margin: 1.6rem 0 3.2rem 0;
|
||||
}
|
||||
|
||||
.post :global(figure) :global(img) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.post :global(figcaption) {
|
||||
color: var(--sk-theme-2);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.post :global(video) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post :global(aside) {
|
||||
float: right;
|
||||
margin: 0 0 1em 1em;
|
||||
width: 16rem;
|
||||
color: var(--sk-theme-2);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.post :global(.max) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post :global(iframe) {
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
margin: 2em 0;
|
||||
border-radius: var(--sk-border-radius);
|
||||
border: 0.8rem solid var(--sk-theme-2);
|
||||
}
|
||||
|
||||
@media (min-width: 910px) {
|
||||
.post :global(.max) {
|
||||
width: calc(100vw - 2 * var(--sk-page-padding-side));
|
||||
margin: 0 calc(var(--sk-page-main-width) / 2 - 50vw);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.post :global(.max) > :global(*) {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.post :global(iframe) {
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
margin: 2em auto;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,51 +0,0 @@
|
||||
import { get_blog_data, get_processed_blog_post } from '$lib/server/blog/index.js';
|
||||
import { Resvg } from '@resvg/resvg-js';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import satori from 'satori';
|
||||
import { html as toReactNode } from 'satori-html';
|
||||
import Card from './Card.svelte';
|
||||
import OverpassRegular from './Overpass-Regular.ttf';
|
||||
|
||||
const height = 630;
|
||||
const width = 1200;
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function GET({ params }) {
|
||||
const post = await get_processed_blog_post(await get_blog_data(), params.slug);
|
||||
|
||||
if (!post) error(404);
|
||||
|
||||
// @ts-ignore
|
||||
const result = Card.render({ post });
|
||||
const element = toReactNode(`${result.html}<style>${result.css.code}</style>`);
|
||||
|
||||
const svg = await satori(element, {
|
||||
fonts: [
|
||||
{
|
||||
name: 'Overpass',
|
||||
data: Buffer.from(OverpassRegular),
|
||||
style: 'normal',
|
||||
weight: 400
|
||||
}
|
||||
],
|
||||
height,
|
||||
width
|
||||
});
|
||||
|
||||
const resvg = new Resvg(svg, {
|
||||
fitTo: {
|
||||
mode: 'width',
|
||||
value: width
|
||||
}
|
||||
});
|
||||
|
||||
const image = resvg.render();
|
||||
|
||||
return new Response(image.asPng(), {
|
||||
headers: {
|
||||
'content-type': 'image/png',
|
||||
'cache-control': 'public, max-age=600' // cache for 10 minutes
|
||||
}
|
||||
});
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
<script>
|
||||
export let post;
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
<img src="https://sveltejs.github.io/assets/artwork/svelte-machine.png" alt="Svelte Machine" />
|
||||
|
||||
<div class="text">
|
||||
<h1>{post.title}</h1>
|
||||
<p class="date">{post.date_formatted}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: 'Overpass';
|
||||
background: white;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
width: 125%;
|
||||
height: 100%;
|
||||
top: 5%;
|
||||
left: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
left: 80px;
|
||||
width: 55%;
|
||||
height: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 72px;
|
||||
margin: 0;
|
||||
color: #222;
|
||||
font-weight: 400;
|
||||
line-height: 80px;
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 32px;
|
||||
margin: 0;
|
||||
color: #555;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
Binary file not shown.
@ -1,72 +0,0 @@
|
||||
import { get_blog_data, get_blog_list } from '$lib/server/blog/index.js';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
const months = ',Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(',');
|
||||
|
||||
/** @param {string} str */
|
||||
function formatPubdate(str) {
|
||||
const [y, m, d] = str.split('-');
|
||||
return `${d} ${months[+m]} ${y} 12:00 +0000`;
|
||||
}
|
||||
|
||||
/** @param {string} html */
|
||||
function escapeHTML(html) {
|
||||
/** @type {{ [key: string]: string }} */
|
||||
const chars = {
|
||||
'"': 'quot',
|
||||
"'": '#39',
|
||||
'&': 'amp',
|
||||
'<': 'lt',
|
||||
'>': 'gt'
|
||||
};
|
||||
|
||||
return html.replace(/["'&<>]/g, (c) => `&${chars[c]};`);
|
||||
}
|
||||
|
||||
/** @param {import('$lib/server/blog/types').BlogPostSummary[]} posts */
|
||||
const get_rss = (posts) =>
|
||||
`
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<rss version="2.0">
|
||||
|
||||
<channel>
|
||||
<title>Svelte blog</title>
|
||||
<link>https://svelte.dev/blog</link>
|
||||
<description>News and information about the magical disappearing UI framework</description>
|
||||
<image>
|
||||
<url>https://svelte.dev/favicon.png</url>
|
||||
<title>Svelte</title>
|
||||
<link>https://svelte.dev/blog</link>
|
||||
</image>
|
||||
${posts
|
||||
.filter((post) => !post.draft)
|
||||
.map(
|
||||
(post) => `
|
||||
<item>
|
||||
<title>${escapeHTML(post.title)}</title>
|
||||
<link>https://svelte.dev/blog/${post.slug}</link>
|
||||
<description>${escapeHTML(post.description)}</description>
|
||||
<pubDate>${formatPubdate(post.date)}</pubDate>
|
||||
</item>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</channel>
|
||||
|
||||
</rss>
|
||||
`
|
||||
.replace(/>[^\S]+/gm, '>')
|
||||
.replace(/[^\S]+</gm, '<')
|
||||
.trim();
|
||||
|
||||
export async function GET() {
|
||||
const posts = get_blog_list(await get_blog_data());
|
||||
|
||||
return new Response(get_rss(posts), {
|
||||
headers: {
|
||||
'Cache-Control': `max-age=${30 * 60 * 1e3}`,
|
||||
'Content-Type': 'application/rss+xml'
|
||||
}
|
||||
});
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
export function load() {
|
||||
redirect(dev ? 307 : 308, '/docs');
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export function GET() {
|
||||
return new Response(undefined, {
|
||||
status: 302,
|
||||
headers: { Location: 'https://discord.gg/svelte' }
|
||||
});
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import { content } from './content.server';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function GET() {
|
||||
return json({
|
||||
blocks: await content()
|
||||
});
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
import { modules } from '$lib/generated/type-info.js';
|
||||
import {
|
||||
extractFrontmatter,
|
||||
markedTransform,
|
||||
normalizeSlugify,
|
||||
removeMarkdown,
|
||||
replaceExportTypePlaceholders
|
||||
} from '@sveltejs/site-kit/markdown';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import glob from 'tiny-glob';
|
||||
import { CONTENT_BASE } from '../../constants.js';
|
||||
|
||||
const base = CONTENT_BASE;
|
||||
|
||||
/** @param {string[]} parts */
|
||||
function get_href(parts) {
|
||||
return parts.length > 1 ? `/docs/${parts[0]}#${parts.at(-1)}` : `/docs/${parts[0]}`;
|
||||
}
|
||||
|
||||
/** @param {string} path */
|
||||
function path_basename(path) {
|
||||
return path.split(/[\\/]/).pop();
|
||||
}
|
||||
|
||||
export async function content() {
|
||||
/** @type {import('@sveltejs/site-kit/search').Block[]} */
|
||||
const blocks = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
const breadcrumbs = [];
|
||||
|
||||
for (const file of await glob('**/*.md', { cwd: `${base}/docs` })) {
|
||||
const basename = path_basename(file);
|
||||
const match = basename && /\d{2}-(.+)\.md/.exec(basename);
|
||||
if (!match) continue;
|
||||
|
||||
const slug = match[1];
|
||||
|
||||
const filepath = `${base}/docs/${file}`;
|
||||
const markdown = await replaceExportTypePlaceholders(
|
||||
await readFile(filepath, 'utf-8'),
|
||||
modules
|
||||
);
|
||||
|
||||
const { body, metadata } = extractFrontmatter(markdown);
|
||||
|
||||
const sections = body.trim().split(/^## /m);
|
||||
const intro = sections?.shift()?.trim();
|
||||
const rank = +metadata.rank;
|
||||
|
||||
if (intro) {
|
||||
blocks.push({
|
||||
breadcrumbs: [...breadcrumbs, removeMarkdown(metadata.title ?? '')],
|
||||
href: get_href([slug]),
|
||||
content: await plaintext(intro),
|
||||
rank
|
||||
});
|
||||
}
|
||||
|
||||
for (const section of sections) {
|
||||
const lines = section.split('\n');
|
||||
const h2 = lines.shift();
|
||||
if (!h2) {
|
||||
console.warn('Could not find expected heading h2');
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = lines.join('\n');
|
||||
const subsections = content.trim().split('## ');
|
||||
const intro = subsections?.shift()?.trim();
|
||||
if (intro) {
|
||||
blocks.push({
|
||||
breadcrumbs: [...breadcrumbs, removeMarkdown(metadata.title), removeMarkdown(h2)],
|
||||
href: get_href([slug, normalizeSlugify(h2)]),
|
||||
content: await plaintext(intro),
|
||||
rank
|
||||
});
|
||||
}
|
||||
|
||||
for (const subsection of subsections) {
|
||||
const lines = subsection.split('\n');
|
||||
const h3 = lines.shift();
|
||||
if (!h3) {
|
||||
console.warn('Could not find expected heading h3');
|
||||
continue;
|
||||
}
|
||||
|
||||
blocks.push({
|
||||
breadcrumbs: [
|
||||
...breadcrumbs,
|
||||
removeMarkdown(metadata.title),
|
||||
removeMarkdown(h2),
|
||||
removeMarkdown(h3)
|
||||
],
|
||||
href: get_href([slug, normalizeSlugify(h2) + '-' + normalizeSlugify(h3)]),
|
||||
content: await plaintext(lines.join('\n').trim()),
|
||||
rank
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/** @param {string} markdown */
|
||||
async function plaintext(markdown) {
|
||||
/** @param {unknown} text */
|
||||
const block = (text) => `${text}\n`;
|
||||
|
||||
/** @param {string} text */
|
||||
const inline = (text) => text;
|
||||
|
||||
return (
|
||||
await markedTransform(markdown, {
|
||||
code: (source) => source.split('// ---cut---\n').pop() || 'ERROR: ---cut--- not found',
|
||||
blockquote: block,
|
||||
html: () => '\n',
|
||||
heading: (text) => `${text}\n`,
|
||||
hr: () => '',
|
||||
list: block,
|
||||
listitem: block,
|
||||
checkbox: block,
|
||||
paragraph: (text) => `${text}\n\n`,
|
||||
table: block,
|
||||
tablerow: block,
|
||||
tablecell: (text, opts) => {
|
||||
return text + ' ';
|
||||
},
|
||||
strong: inline,
|
||||
em: inline,
|
||||
codespan: inline,
|
||||
br: () => '',
|
||||
del: inline,
|
||||
link: (href, title, text) => text,
|
||||
image: (href, title, text) => text,
|
||||
text: inline
|
||||
})
|
||||
)
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&#(\d+);/g, (match, code) => {
|
||||
return String.fromCharCode(code);
|
||||
})
|
||||
.trim();
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
export const prerender = true;
|
||||
|
||||
export async function load({ url }) {
|
||||
if (url.pathname === '/docs') {
|
||||
return {
|
||||
sections: []
|
||||
};
|
||||
}
|
||||
|
||||
const { get_docs_data, get_docs_list } = await import('$lib/server/docs/index.js');
|
||||
|
||||
return {
|
||||
sections: get_docs_list(await get_docs_data())
|
||||
};
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { DocsContents } from '@sveltejs/site-kit/docs';
|
||||
|
||||
export let data;
|
||||
|
||||
$: pageData = $page.data.page;
|
||||
|
||||
$: title = pageData?.title;
|
||||
$: category = pageData?.category;
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="toc-container" style="order: 1">
|
||||
<DocsContents contents={data.sections} />
|
||||
</div>
|
||||
|
||||
<div class="page content">
|
||||
{#if category}
|
||||
<p class="category">{category}</p>
|
||||
{/if}
|
||||
{#if title}
|
||||
<h1>{title}</h1>
|
||||
{/if}
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
--sidebar-menu-width: 28rem;
|
||||
--sidebar-width: var(--sidebar-menu-width);
|
||||
--ts-toggle-height: 4.2rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.page {
|
||||
padding: var(--sk-page-padding-top) var(--sk-page-padding-side);
|
||||
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.page :global(:where(h2, h3) code) {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
.category {
|
||||
font: 700 var(--sk-text-s) var(--sk-font);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
margin: 0 0 0.5em;
|
||||
color: var(--sk-text-3);
|
||||
}
|
||||
|
||||
@media (min-width: 832px) {
|
||||
.content {
|
||||
padding-left: calc(var(--sidebar-width) + var(--sk-page-padding-side));
|
||||
}
|
||||
}
|
||||
|
||||
.toc-container {
|
||||
background: var(--sk-back-3);
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 832px) {
|
||||
.toc-container {
|
||||
display: block;
|
||||
width: var(--sidebar-width);
|
||||
height: calc(100vh - var(--sk-nav-height) - var(--sk-banner-bottom-height));
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: var(--sk-nav-height);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.toc-container::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: calc(var(--sidebar-width) - 1px);
|
||||
border-right: 1px solid var(--sk-back-5);
|
||||
}
|
||||
|
||||
.page {
|
||||
padding-left: calc(var(--sidebar-width) + var(--sk-page-padding-side));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.container {
|
||||
--sidebar-width: max(28rem, 23vw);
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.page {
|
||||
--on-this-page-display: block;
|
||||
padding: var(--sk-page-padding-top) calc(var(--sidebar-width) + var(--sk-page-padding-side));
|
||||
margin: 0 auto;
|
||||
max-width: var(--sk-line-max-width);
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,216 +0,0 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const OLD_IDs = [
|
||||
'before-we-begin',
|
||||
'getting-started',
|
||||
'component-format',
|
||||
'component-format-script',
|
||||
'component-format-script-1-export-creates-a-component-prop',
|
||||
'component-format-script-2-assignments-are-reactive',
|
||||
'component-format-script-3-$-marks-a-statement-as-reactive',
|
||||
'component-format-script-4-prefix-stores-with-$-to-access-their-values',
|
||||
'component-format-script-context-module',
|
||||
'component-format-style',
|
||||
'template-syntax',
|
||||
'template-syntax-tags',
|
||||
'template-syntax-attributes-and-props',
|
||||
'template-syntax-text-expressions',
|
||||
'template-syntax-comments',
|
||||
'template-syntax-if',
|
||||
'template-syntax-each',
|
||||
'template-syntax-await',
|
||||
'template-syntax-key',
|
||||
'template-syntax-html',
|
||||
'template-syntax-debug',
|
||||
'template-syntax-const',
|
||||
'template-syntax-element-directives',
|
||||
'template-syntax-element-directives-on-eventname',
|
||||
'template-syntax-element-directives-bind-property',
|
||||
'template-syntax-element-directives-bind-group',
|
||||
'template-syntax-element-directives-bind-this',
|
||||
'template-syntax-element-directives-class-name',
|
||||
'template-syntax-element-directives-style-property',
|
||||
'template-syntax-element-directives-use-action',
|
||||
'template-syntax-element-directives-transition-fn',
|
||||
'template-syntax-element-directives-in-fn-out-fn',
|
||||
'template-syntax-element-directives-animate-fn',
|
||||
'template-syntax-component-directives',
|
||||
'template-syntax-component-directives-on-eventname',
|
||||
'template-syntax-component-directives---style-props',
|
||||
'template-syntax-component-directives-bind-property',
|
||||
'template-syntax-component-directives-bind-this',
|
||||
'template-syntax-slot',
|
||||
'template-syntax-slot-slot-name-name',
|
||||
'template-syntax-slot-$$slots',
|
||||
'template-syntax-slot-slot-key-value',
|
||||
'template-syntax-svelte-self',
|
||||
'template-syntax-svelte-component',
|
||||
'template-syntax-svelte-element',
|
||||
'template-syntax-svelte-window',
|
||||
'template-syntax-svelte-document',
|
||||
'template-syntax-svelte-body',
|
||||
'template-syntax-svelte-head',
|
||||
'template-syntax-svelte-options',
|
||||
'template-syntax-svelte-fragment',
|
||||
'run-time',
|
||||
'run-time-svelte',
|
||||
'run-time-svelte-onmount',
|
||||
'run-time-svelte-beforeupdate',
|
||||
'run-time-svelte-afterupdate',
|
||||
'run-time-svelte-ondestroy',
|
||||
'run-time-svelte-tick',
|
||||
'run-time-svelte-setcontext',
|
||||
'run-time-svelte-getcontext',
|
||||
'run-time-svelte-hascontext',
|
||||
'run-time-svelte-getallcontexts',
|
||||
'run-time-svelte-createeventdispatcher',
|
||||
'run-time-svelte-store',
|
||||
'run-time-svelte-store-writable',
|
||||
'run-time-svelte-store-readable',
|
||||
'run-time-svelte-store-derived',
|
||||
'run-time-svelte-store-get',
|
||||
'run-time-svelte-store-readonly',
|
||||
'run-time-svelte-motion',
|
||||
'run-time-svelte-motion-tweened',
|
||||
'run-time-svelte-motion-spring',
|
||||
'run-time-svelte-transition',
|
||||
'run-time-svelte-transition-fade',
|
||||
'run-time-svelte-transition-blur',
|
||||
'run-time-svelte-transition-fly',
|
||||
'run-time-svelte-transition-slide',
|
||||
'run-time-svelte-transition-scale',
|
||||
'run-time-svelte-transition-draw',
|
||||
'run-time-svelte-transition-crossfade',
|
||||
'run-time-svelte-animate',
|
||||
'run-time-svelte-animate-flip',
|
||||
'run-time-svelte-easing',
|
||||
'run-time-svelte-register',
|
||||
'run-time-client-side-component-api',
|
||||
'run-time-client-side-component-api-creating-a-component',
|
||||
'run-time-client-side-component-api-$set',
|
||||
'run-time-client-side-component-api-$on',
|
||||
'run-time-client-side-component-api-$destroy',
|
||||
'run-time-client-side-component-api-component-props',
|
||||
'run-time-custom-element-api',
|
||||
'run-time-server-side-component-api',
|
||||
'compile-time',
|
||||
'compile-time-svelte-compile',
|
||||
'compile-time-svelte-parse',
|
||||
'compile-time-svelte-preprocess',
|
||||
'compile-time-svelte-walk',
|
||||
'compile-time-svelte-version',
|
||||
'accessibility-warnings',
|
||||
'accessibility-warnings-a11y-accesskey',
|
||||
'accessibility-warnings-a11y-aria-activedescendant-has-tabindex',
|
||||
'accessibility-warnings-a11y-aria-attributes',
|
||||
'accessibility-warnings-a11y-autofocus',
|
||||
'accessibility-warnings-a11y-click-events-have-key-events',
|
||||
'accessibility-warnings-a11y-distracting-elements',
|
||||
'accessibility-warnings-a11y-hidden',
|
||||
'accessibility-warnings-a11y-img-redundant-alt',
|
||||
'accessibility-warnings-a11y-incorrect-aria-attribute-type',
|
||||
'accessibility-warnings-a11y-invalid-attribute',
|
||||
'accessibility-warnings-a11y-interactive-supports-focus',
|
||||
'accessibility-warnings-a11y-label-has-associated-control',
|
||||
'accessibility-warnings-a11y-media-has-caption',
|
||||
'accessibility-warnings-a11y-misplaced-role',
|
||||
'accessibility-warnings-a11y-misplaced-scope',
|
||||
'accessibility-warnings-a11y-missing-attribute',
|
||||
'accessibility-warnings-a11y-missing-content',
|
||||
'accessibility-warnings-a11y-mouse-events-have-key-events',
|
||||
'accessibility-warnings-a11y-no-redundant-roles',
|
||||
'accessibility-warnings-a11y-no-interactive-element-to-noninteractive-role',
|
||||
'accessibility-warnings-a11y-no-noninteractive-element-to-interactive-role',
|
||||
'accessibility-warnings-a11y-no-noninteractive-tabindex',
|
||||
'accessibility-warnings-a11y-positive-tabindex',
|
||||
'accessibility-warnings-a11y-role-has-required-aria-props',
|
||||
'accessibility-warnings-a11y-role-supports-aria-props',
|
||||
'accessibility-warnings-a11y-structure',
|
||||
'accessibility-warnings-a11y-unknown-aria-attribute',
|
||||
'accessibility-warnings-a11y-unknown-role'
|
||||
];
|
||||
|
||||
/** @type {Map<RegExp, string>}*/
|
||||
const pages_regex_map = new Map([
|
||||
// Basic ones
|
||||
[/(before-we-begin|getting-started)$/i, 'introduction'],
|
||||
[/template-syntax$/i, 'basic-markup'],
|
||||
[/component-format$/i, 'svelte-components'],
|
||||
[/run-time$/i, 'svelte'],
|
||||
[/compile-time$/i, 'svelte-compiler'],
|
||||
[/(accessibility-warnings)$/i, '$1'],
|
||||
|
||||
// component-format-
|
||||
[/component-format-(script|style|script-context-module)$/i, 'svelte-components#$1'],
|
||||
[/component-format-(script)(?:-?(.*))$/i, 'svelte-components#$1-$2'],
|
||||
|
||||
// template-syntax
|
||||
[/template-syntax-((?:element|component)-directives)-?(.*)/i, '$1#$2'],
|
||||
[/template-syntax-slot$/i, 'special-elements#slot'],
|
||||
[/template-syntax-(slot)-?(.*)/i, 'special-elements#$1-$2'],
|
||||
[/template-syntax-(if|each|await|key)$/i, 'logic-blocks#$1'],
|
||||
[/template-syntax-(const|debug|html)$/i, 'special-tags#$1'],
|
||||
[/template-syntax-(tags|attributes-and-props|text-expressions|comments)$/i, 'basic-markup#$1'],
|
||||
// !!!! This one should stay at the bottom of `template-syntax`, or it may end up hijacking logic blocks and special tags
|
||||
[/template-syntax-(.+)/i, 'special-elements#$1'],
|
||||
|
||||
// run-time
|
||||
[/run-time-(svelte-(?:store|motion|transition|animate))-?(.*)/i, '$1#$2'],
|
||||
[/run-time-(client-side-component-api)-?(.*)/i, '$1#$2'],
|
||||
[
|
||||
/run-time-(svelte-easing|server-side-component-api|custom-element-api|svelte-register)$/i,
|
||||
'$1'
|
||||
],
|
||||
// Catch all, should be at the end or will include store, motion, transition and other modules starting with svelte
|
||||
[/run-time-(svelte)(?:-(.+))?/i, '$1#$2'],
|
||||
|
||||
// Compile time
|
||||
[/compile-time-svelte-?(.*)/i, 'svelte-compiler#$1'],
|
||||
|
||||
// Accessibility warnings
|
||||
[/(accessibility-warnings)-?(.+)/i, '$1#$2']
|
||||
]);
|
||||
|
||||
function get_old_new_ids_map() {
|
||||
/** @type {Map<string, string>} */
|
||||
const new_ids = new Map();
|
||||
|
||||
old_id_block: for (const old_id of OLD_IDs) {
|
||||
for (const [regex, replacement] of pages_regex_map) {
|
||||
if (regex.test(old_id)) {
|
||||
new_ids.set(
|
||||
old_id,
|
||||
old_id
|
||||
.replace(regex, replacement)
|
||||
.replace(/#$/, '') // Replace trailing # at the end
|
||||
.replace('#--', '#') // have to do the -- replacement because of `--style-props` in old being `style-props` in new
|
||||
);
|
||||
continue old_id_block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new_ids;
|
||||
}
|
||||
|
||||
function get_url_to_redirect_to() {
|
||||
const hash = $page.url.hash.replace(/^#/i, '');
|
||||
|
||||
if (!hash) return '/docs/introduction';
|
||||
|
||||
const old_new_map = get_old_new_ids_map();
|
||||
|
||||
// ID doesn't match anything, take the user to intro page only
|
||||
if (!old_new_map.has(hash)) return '/docs/introduction';
|
||||
|
||||
return `/docs/${old_new_map.get(hash)}`;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
console.log(get_old_new_ids_map()); // for debugging purposes in prod
|
||||
goto(get_url_to_redirect_to(), { replaceState: true });
|
||||
});
|
||||
</script>
|
@ -1,12 +0,0 @@
|
||||
import { get_docs_data, get_parsed_docs } from '$lib/server/docs/index.js';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function load({ params }) {
|
||||
const processed_page = await get_parsed_docs(await get_docs_data(), params.slug);
|
||||
|
||||
if (!processed_page) error(404);
|
||||
|
||||
return { page: processed_page };
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { Icon } from '@sveltejs/site-kit/components';
|
||||
import { copy_code_descendants } from '@sveltejs/site-kit/actions';
|
||||
import { DocsOnThisPage, setupDocsHovers } from '@sveltejs/site-kit/docs';
|
||||
|
||||
export let data;
|
||||
|
||||
$: pages = data.sections.flatMap((section) => section.pages);
|
||||
$: index = pages.findIndex(({ path }) => path === $page.url.pathname);
|
||||
$: prev = pages[index - 1];
|
||||
$: next = pages[index + 1];
|
||||
|
||||
setupDocsHovers();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.page?.title} • Docs • Svelte</title>
|
||||
|
||||
<meta name="twitter:title" content="{data.page.title} • Docs • Svelte" />
|
||||
<meta name="twitter:description" content="{data.page.title} • Svelte documentation" />
|
||||
<meta name="description" content="{data.page.title} • Svelte documentation" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="text" id="docs-content" use:copy_code_descendants>
|
||||
<a
|
||||
class="edit"
|
||||
href="https://github.com/sveltejs/svelte/edit/master/documentation/docs/{data.page.file}"
|
||||
>
|
||||
<Icon size={50} name="edit" /> Edit this page on GitHub
|
||||
</a>
|
||||
|
||||
<DocsOnThisPage details={data.page} />
|
||||
|
||||
{@html data.page.content}
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div>
|
||||
<span class:faded={!prev}>previous</span>
|
||||
|
||||
{#if prev}
|
||||
<a href={prev.path}>{prev.title}</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class:faded={!next}>next</span>
|
||||
{#if next}
|
||||
<a href={next.path}>{next.title}</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.edit {
|
||||
position: relative;
|
||||
font-size: 1.4rem;
|
||||
line-height: 1;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.edit :global(.icon) {
|
||||
position: relative;
|
||||
top: -0.1rem;
|
||||
left: 0.3rem;
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
max-width: calc(var(--sk-line-max-width) + 1rem);
|
||||
border-top: 1px solid var(--sk-back-4);
|
||||
padding: 1rem 0 0 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
margin: 6rem 0 0 0;
|
||||
}
|
||||
|
||||
.controls > :first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.controls > :last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.controls span {
|
||||
display: block;
|
||||
font-size: 1.2rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
color: var(--sk-text-3);
|
||||
}
|
||||
|
||||
.controls span.faded {
|
||||
opacity: 0.4;
|
||||
}
|
||||
</style>
|
@ -1,7 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export function load() {
|
||||
redirect(301, 'examples/hello-world');
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { get_example, get_examples_list } from '$lib/server/examples/index.js';
|
||||
import examples_data from '$lib/generated/examples-data.js';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function load({ params }) {
|
||||
const examples_list = get_examples_list(examples_data);
|
||||
const example = get_example(examples_data, params.slug);
|
||||
|
||||
return {
|
||||
examples_list,
|
||||
example,
|
||||
slug: params.slug
|
||||
};
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue