Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: shikijs/shiki
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.15.2
Choose a base ref
...
head repository: shikijs/shiki
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.16.0
Choose a head ref
  • 9 commits
  • 85 files changed
  • 2 contributors

Commits on Aug 31, 2024

  1. Verified

    This commit was signed with the committer’s verified signature.
    antfu Anthony Fu
    Copy the full SHA
    54d1963 View commit details
  2. refactor: reorganize code

    antfu committed Aug 31, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    antfu Anthony Fu
    Copy the full SHA
    eb842a3 View commit details

Commits on Sep 1, 2024

  1. feat: synchronous Shiki usage (#764)

    antfu authored Sep 1, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    9396a6f View commit details
  2. chore: update deps, fix docs

    antfu committed Sep 1, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    antfu Anthony Fu
    Copy the full SHA
    6983ac4 View commit details
  3. feat(core): new options object signature for `createdBundledHighlight…

    …er` for custom engine bundle, deprecate old signature
    antfu committed Sep 1, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    antfu Anthony Fu
    Copy the full SHA
    513224f View commit details
  4. fix(core): createdBundledHighlighter allow user to override engine

    antfu committed Sep 1, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    antfu Anthony Fu
    Copy the full SHA
    b59e487 View commit details
  5. docs: docs for js engine and sync usage

    antfu committed Sep 1, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    antfu Anthony Fu
    Copy the full SHA
    e3bee32 View commit details
  6. chore: update gammars

    antfu committed Sep 1, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    antfu Anthony Fu
    Copy the full SHA
    828081e View commit details
  7. chore: release v1.16.0

    antfu committed Sep 1, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    antfu Anthony Fu
    Copy the full SHA
    ab6e602 View commit details
Showing with 12,880 additions and 793 deletions.
  1. +0 −6 .github/workflows/ci.yml
  2. +0 −3 .gitmodules
  3. +0 −16 CONTRIBUTING.md
  4. +30 −26 bench/engines.bench.ts
  5. +6 −3 docs/.vitepress/config.ts
  6. +48 −3 docs/guide/bundles.md
  7. +1 −1 docs/guide/index.md
  8. +4 −41 docs/guide/install.md
  9. +57 −0 docs/guide/regex-engines.md
  10. +40 −0 docs/guide/sync-usage.md
  11. +1 −1 docs/packages/markdown-it.md
  12. +1 −1 docs/packages/next.md
  13. +1 −1 docs/packages/rehype.md
  14. +250 −0 docs/references/engine-js-compat.md
  15. +2 −2 package.json
  16. +1 −1 packages/cli/package.json
  17. +1 −1 packages/compat/package.json
  18. +2 −1 packages/core/package.json
  19. +0 −1 packages/core/rollup.config.mjs
  20. +54 −7 packages/core/src/{ → constructors}/bundle-factory.ts
  21. +31 −7 packages/core/src/{ → constructors}/highlighter.ts
  22. +40 −57 packages/core/src/{internal.ts → constructors/internal-sync.ts}
  23. +48 −0 packages/core/src/constructors/internal.ts
  24. +1 −2 packages/core/src/engines/javascript.ts
  25. +1 −1 packages/core/src/engines/oniguruma/index.ts
  26. +1 −2 packages/core/src/engines/wasm.ts
  27. +2 −2 packages/core/src/{transformers.ts → highlight/_get-transformers.ts}
  28. +4 −4 packages/core/src/{ → highlight}/code-to-hast.ts
  29. +2 −2 packages/core/src/{ → highlight}/code-to-html.ts
  30. +3 −3 packages/core/src/{ → highlight}/code-to-tokens-ansi.ts
  31. +7 −7 packages/core/src/{ → highlight}/code-to-tokens-base.ts
  32. +1 −1 packages/core/src/{ → highlight}/code-to-tokens-themes.ts
  33. +4 −4 packages/core/src/{ → highlight}/code-to-tokens.ts
  34. +22 −11 packages/core/src/index.ts
  35. +0 −27 packages/core/src/textmate.ts
  36. +25 −0 packages/core/src/textmate/getters-resolve.ts
  37. +3 −4 packages/core/src/{ → textmate}/grammar-state.ts
  38. +1 −1 packages/core/src/{normalize.ts → textmate/normalize-theme.ts}
  39. +12 −17 packages/core/src/{ → textmate}/registry.ts
  40. +11 −13 packages/core/src/{ → textmate}/resolver.ts
  41. +1 −1 packages/core/src/{ → textmate}/stack-element-metadata.ts
  42. +10 −1 packages/core/src/transformer-decorations.ts
  43. +14 −1 packages/core/src/types/engines.ts
  44. +20 −0 packages/core/src/types/highlighter.ts
  45. +1 −6 packages/core/src/types/index.ts
  46. +14 −13 packages/core/src/types/options.ts
  47. +1 −1 packages/core/src/types/textmate.ts
  48. +2 −2 packages/core/src/types/tokens.ts
  49. +20 −1 packages/core/src/utils.ts
  50. +0 −1 packages/core/vendor/vscode-textmate
  51. +1 −1 packages/markdown-it/package.json
  52. +5 −3 packages/markdown-it/test/index.test.ts
  53. +3 −2 packages/monaco/package.json
  54. +3 −2 packages/monaco/src/index.ts
  55. +1 −1 packages/rehype/package.json
  56. +2 −1 packages/shiki/package.json
  57. +7 −7 packages/shiki/src/bundle-full.ts
  58. +7 −7 packages/shiki/src/bundle-web.ts
  59. +56 −0 packages/shiki/test/core-sync.test.ts
  60. +7 −7 packages/shiki/test/core.test.ts
  61. +2 −2 packages/shiki/test/engine-js/__records__/toml.json
  62. +1 −2 packages/shiki/test/engine-js/compare.test.ts
  63. +1 −1 packages/shiki/test/engine-js/types.ts
  64. +1 −1 packages/shiki/test/themes.test.ts
  65. +1 −1 packages/transformers/package.json
  66. +1 −1 packages/twoslash/package.json
  67. +2 −1 packages/twoslash/test/out/classic/compiler_errors.html
  68. +2 −1 packages/twoslash/test/out/classic/console_log.html
  69. +55 −1 packages/twoslash/test/out/completion-end-multifile-2.ts.html
  70. +57 −0 packages/twoslash/test/out/completion-end-multifile-2.ts.json
  71. +55 −1 packages/twoslash/test/out/completion-end.ts.html
  72. +57 −0 packages/twoslash/test/out/completion-end.ts.json
  73. +68 −1 packages/twoslash/test/out/highlights.ts.html
  74. +234 −3 packages/twoslash/test/out/highlights.ts.json
  75. +2 −2 packages/twoslash/test/out/rich/custom-tags.html
  76. +1 −1 packages/vitepress-twoslash/package.json
  77. +5 −2 packages/vitepress-twoslash/test/fixtures.test.ts
  78. +3,419 −0 packages/vitepress-twoslash/test/out/completion-end-multifile-2.ts.json
  79. +3,419 −0 packages/vitepress-twoslash/test/out/completion-end.ts.json
  80. +4,366 −35 packages/vitepress-twoslash/test/out/highlights.ts.json
  81. +179 −138 pnpm-lock.yaml
  82. +12 −11 pnpm-workspace.yaml
  83. +0 −230 scripts/report-engine-js-compat.md
  84. +47 −19 scripts/report-engine-js-compat.ts
  85. +0 −1 tsconfig.json
6 changes: 0 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -14,8 +14,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install pnpm
uses: pnpm/action-setup@v2
@@ -38,8 +36,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install pnpm
uses: pnpm/action-setup@v2
@@ -75,8 +71,6 @@ jobs:

steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install pnpm
uses: pnpm/action-setup@v3
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +0,0 @@
[submodule "packages/core/vendor/vscode-textmate"]
path = packages/core/vendor/vscode-textmate
url = https://github.com/microsoft/vscode-textmate
16 changes: 0 additions & 16 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -4,22 +4,6 @@ Thanks for lending a hand 👋

## Development

### Clone

This repository contains a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) to `vscode-textmate`. By default `git clone` does not clone submodules. To clone this repository and its submodules, use:

```bash
git clone --recursive https://github.com/shikijs/shiki
```

Or if you have already cloned it, use:

```bash
git submodule update --init --recursive
```

Learn more at this [StackOverflow thread](https://stackoverflow.com/a/4438292).

### Setup

- We use [pnpm](https://pnpm.js.org/) to manage dependencies. Install it with `npm i -g pnpm`.
56 changes: 30 additions & 26 deletions bench/engines.bench.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,43 @@
/* eslint-disable no-console */
import fs from 'node:fs/promises'
import { bench, describe } from 'vitest'
import type { BundledLanguage } from 'shiki'
import { createHighlighter, createJavaScriptRegexEngine, createWasmOnigEngine } from 'shiki'
import type { ReportItem } from '../scripts/report-engine-js-compat'

describe('engines', async () => {
const js = createJavaScriptRegexEngine()
const wasm = await createWasmOnigEngine(() => import('shiki/wasm'))
const js = createJavaScriptRegexEngine()
const wasm = await createWasmOnigEngine(() => import('shiki/wasm'))

// Run `npx jiti scripts/report-engine-js-compat.ts` to generate the report first
const report = await fs.readFile('../scripts/report-engine-js-compat.json', 'utf-8').then(JSON.parse) as ReportItem[]
const langs = report.filter(i => i.highlightMatch === true).map(i => i.lang) as BundledLanguage[]
const samples = await Promise.all(langs.map(lang => fs.readFile(`../tm-grammars-themes/samples/${lang}.sample`, 'utf-8')))
const RANGE = [0, 20]

const shikiJs = await createHighlighter({
langs,
themes: ['vitesse-dark'],
engine: js,
})
// Run `npx jiti scripts/report-engine-js-compat.ts` to generate the report first
const report = await fs.readFile(new URL('../scripts/report-engine-js-compat.json', import.meta.url), 'utf-8').then(JSON.parse) as ReportItem[]
const langs = report.filter(i => i.highlightMatch === true).map(i => i.lang).slice(...RANGE) as BundledLanguage[]
// Clone https://github.com/shikijs/textmate-grammars-themes to `../tm-grammars-themes`
const samples = await Promise.all(langs.map(lang => fs.readFile(`../tm-grammars-themes/samples/${lang}.sample`, 'utf-8')))

const shikiWasm = await createHighlighter({
langs,
themes: ['vitesse-dark'],
engine: wasm,
})
console.log('Benchmarking engines with', langs.length, 'languages')

const shikiJs = await createHighlighter({
langs,
themes: ['vitesse-dark'],
engine: js,
})

bench('js', () => {
for (const lang of langs) {
const shikiWasm = await createHighlighter({
langs,
themes: ['vitesse-dark'],
engine: wasm,
})

for (const lang of langs) {
describe(lang, () => {
bench('js', () => {
shikiJs.codeToTokensBase(samples[langs.indexOf(lang)], { lang, theme: 'vitesse-dark' })
}
}, { warmupIterations: 10, iterations: 30 })
})

bench('wasm', () => {
for (const lang of langs) {
bench('wasm', () => {
shikiWasm.codeToTokensBase(samples[langs.indexOf(lang)], { lang, theme: 'vitesse-dark' })
}
}, { warmupIterations: 10, iterations: 30 })
})
})
})
}
9 changes: 6 additions & 3 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -11,21 +11,24 @@ import vite from './vite.config'

const GUIDES: DefaultTheme.NavItemWithLink[] = [
{ text: 'Getting Started', link: '/guide/' },
{ text: 'Installation', link: '/guide/install' },
{ text: 'Installation & Usage', link: '/guide/install' },
{ text: 'Bundles', link: '/guide/bundles' },
{ text: 'Dual Themes', link: '/guide/dual-themes' },
{ text: 'Decorations', link: '/guide/decorations' },
{ text: 'Transformers', link: '/guide/transformers' },
{ text: 'Theme Colors Manipulation', link: '/guide/theme-colors' },
{ text: 'Migration', link: '/guide/migrate' },
{ text: 'Compatibility Build', link: '/guide/compat' },
{ text: 'RegExp Engines', link: '/guide/regex-engines' },
{ text: 'Synchronous Usage', link: '/guide/sync-usage' },
{ text: 'Custom Themes', link: '/guide/load-theme' },
{ text: 'Custom Languages', link: '/guide/load-lang' },
{ text: 'Migration', link: '/guide/migrate' },
{ text: 'Compatibility Build', link: '/guide/compat' },
]

const REFERENCES: DefaultTheme.NavItemWithLink[] = [
{ text: 'Themes', link: '/themes' },
{ text: 'Languages', link: '/languages' },
{ text: 'JavaScript Engine Compatibility', link: '/references/engine-js-compat' },
]

const INTEGRATIONS: DefaultTheme.NavItemWithLink[] = [
51 changes: 48 additions & 3 deletions docs/guide/bundles.md
Original file line number Diff line number Diff line change
@@ -4,17 +4,19 @@ outline: deep

# Bundles

The main `shiki` entries bundles all supported themes and languages via lazy dynamic imports. The efficiency shouldn't be a concern to most of the scenarios as the grammar would only be imported/downloaded when it is used. However, when you bundle Shiki into browsers runtime or web workers, even those files are not imported, they still add up to your dist size. We provide the [fine-grained bundle](/guide/install#fine-grained-bundle) to help you compose languages and themes one-by-one as you need.
The main `shiki` entries bundles all supported themes and languages via lazy dynamic imports. The efficiency shouldn't be a concern to most of the scenarios as the grammar would only be imported/downloaded when it is used. However, when you bundle Shiki into browsers runtime or web workers, even those files are not imported, they still add up to your dist size. We provide the [fine-grained bundle](#fine-grained-bundle) to help you compose languages and themes one-by-one as you need.

## Bundle Presets

To make it easier, we also provide some pre-composed bundles for you to use:

## `shiki/bundle/full`
### `shiki/bundle/full`

> [Bundle Size](/guide/#bundle-size): 6.4 MB (minified), 1.2 MB (gzip), async chunks included
The full bundle includes all themes and languages, same as the main `shiki` entry.

## `shiki/bundle/web`
### `shiki/bundle/web`

> [Bundle Size](/guide/#bundle-size): 3.8 MB (minified), 695 KB (gzip), async chunks included
@@ -35,3 +37,46 @@ const highlighter = await createHighlighter({
themes: ['github-dark', 'github-light'],
})
```

## Fine-grained Bundle

When importing `shiki`, all the themes and languages are bundled as async chunks. Normally it won't be a concern to you as they are not being loaded if you don't use them. In some cases, if you want to control what to bundle, you can use the core and compose your own bundle.

```ts twoslash
// @noErrors
// `shiki/core` entry does not include any themes or languages or the wasm binary.
import { createHighlighterCore } from 'shiki/core'

// directly import the theme and language modules, only the ones you imported will be bundled.
import nord from 'shiki/themes/nord.mjs'

const highlighter = await createHighlighterCore({
themes: [
// instead of strings, you need to pass the imported module
nord,
// or a dynamic import if you want to do chunk splitting
import('shiki/themes/material-theme-ocean.mjs')
],
langs: [
import('shiki/langs/javascript.mjs'),
// shiki will try to interop the module with the default export
() => import('shiki/langs/css.mjs'),
// or a getter that returns custom grammar
async () => JSON.parse(await fs.readFile('my-grammar.json', 'utf-8'))
],
// `shiki/wasm` contains the wasm binary inlined as base64 string.
engine: createWasmOnigEngine(import('shiki/wasm'))
})

// optionally, load themes and languages after creation
await highlighter.loadTheme(import('shiki/themes/vitesse-light.mjs'))

const code = highlighter.codeToHtml('const a = 1', {
lang: 'javascript',
theme: 'material-theme-ocean'
})
```

::: info
[Shorthands](/guide/install#shorthands) are only avaliable in bundle presets. For a fine-grained bundle, you can create your own shorthands using [`createSingletonShorthands`](https://github.com/shikijs/shiki/blob/main/packages/core/src/constructors/bundle-factory.ts#L203) or port it yourself.
:::
2 changes: 1 addition & 1 deletion docs/guide/index.md
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ Oh by the way, all the code blocks in this site are highlighted by Shiki, as you
- All grammars/themes/wasm served as ESM, lazy-loaded on demand and bundler-friendly.
- Portable & agnostic. Does not rely on Node.js APIs or the filesystem, works in any modern JavaScript runtime.
- ESM-only ([CDN Usage](/guide/install#cdn-usage), [CJS Usage](/guide/install#cjs-usage)).
- [Bundles languages/themes composedly](/guide/install#fine-grained-bundle).
- [Bundles languages/themes composedly](/guide/bundles#fine-grained-bundle).
- [Light/Dark themes support](/guide/dual-themes)
- [`hast` support](/guide/transformers#codetohast)
- [Transformers API](/guide/transformers)
45 changes: 4 additions & 41 deletions docs/guide/install.md
Original file line number Diff line number Diff line change
@@ -2,10 +2,12 @@
outline: deep
---

# Installation
# Installation & Usage

<Badges name="shiki" />

## Installation

Install via npm, or see [CDN Usage](#cdn-usage):
::: code-group

@@ -160,46 +162,7 @@ highlighter.codeToHtml('const a = 1', {

When importing `shiki`, all the themes and languages are bundled as async chunks. Normally it won't be a concern to you as they are not being loaded if you don't use them. In some cases, if you want to control what to bundle, you can use the core and compose your own bundle.

```ts twoslash theme:material-theme-ocean
// @noErrors
// `shiki/core` entry does not include any themes or languages or the wasm binary.
import { createHighlighterCore } from 'shiki/core'

// `shiki/wasm` contains the wasm binary inlined as base64 string.
import getWasm from 'shiki/wasm'

// directly import the theme and language modules, only the ones you imported will be bundled.
import nord from 'shiki/themes/nord.mjs'

const highlighter = await createHighlighterCore({
themes: [
// instead of strings, you need to pass the imported module
nord,
// or a dynamic import if you want to do chunk splitting
import('shiki/themes/material-theme-ocean.mjs')
],
langs: [
import('shiki/langs/javascript.mjs'),
// shiki will try to interop the module with the default export
() => import('shiki/langs/css.mjs'),
// or a getter that returns custom grammar
async () => JSON.parse(await fs.readFile('my-grammar.json', 'utf-8'))
],
loadWasm: getWasm
})

// optionally, load themes and languages after creation
await highlighter.loadTheme(import('shiki/themes/vitesse-light.mjs'))

const code = highlighter.codeToHtml('const a = 1', {
lang: 'javascript',
theme: 'material-theme-ocean'
})
```

::: info
[Shorthands](#shorthands) are only avaliable in [bundled usage](#bundled-usage). For a fine-grained bundle, you can create your own shorthands using [`createSingletonShorthands`](https://github.com/shikijs/shiki/blob/main/packages/core/src/bundle-factory.ts) or port it yourself.
:::
Check out the [Fine-grained Bundle](/guide/bundles#fine-grained-bundle) section for more details.

### Bundle Presets

57 changes: 57 additions & 0 deletions docs/guide/regex-engines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
outline: deep
---

# RegExp Engines

TextMate grammars is based on regular expressions to match tokens. Usually, we use [Oniguruma](https://github.com/kkos/oniguruma) (a regular expression engine written in C) to parse the grammar. To make it work in JavaScript, we compile Oniguruma to WebAssembly to run in the browser or Node.js.

Since v1.15, we expose the ability to for users to switch the RegExp engine and provide custom implementations.

An `engine` option is added to the `createHighlighter` and `createHighlighterCore`. For example:

```ts
import { createHighlighter } from 'shiki'

const shiki = await createShiki({
themes: ['nord'],
langs: ['javascript'],
engine: { /* custom engine */ }
})
```

Shiki come with two built-in engines:

## Oniguruma Engine

This is the default engine that uses the compiled Oniguruma WebAssembly. The most accurate and robust engine.

```ts
import { createHighlighter, createWasmOnigEngine } from 'shiki'

const shiki = await createShiki({
themes: ['nord'],
langs: ['javascript'],
engine: createWasmOnigEngine(import('shiki/wasm'))
})
```

## JavaScript RegExp Engine (Experimental)

This experimental engine uses JavaScript's native RegExp. As TextMate grammars' regular expressions are in Oniguruma flavor that might contains syntaxes that are not supported by JavaScript's RegExp, we use [`oniguruma-to-js`](https://github.com/antfu/oniguruma-to-js) to lowering the syntaxes and try to make them compatible with JavaScript's RegExp.

Please check the [compatibility table](/references/engine-js-compat) to check the support status of the languages you are using.

```ts {3,8}
import { createHighlighter, createJavaScriptRegexEngine } from 'shiki'

const jsEngine = createJavaScriptRegexEngine()

const shiki = await createHighlighter({
themes: ['nord'],
langs: ['javascript'],
engine: jsEngine
})

const html = shiki.codeToHtml('const a = 1', { lang: 'javascript', theme: 'nord' })
```
40 changes: 40 additions & 0 deletions docs/guide/sync-usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Synchronous Usage

The `await createHighlighter()` and `highlighter.codeToHtml()` are already the effort to do the seperations of asynchronism and synchronism. For most of the cases, you should be able to resolve the async part in the initialization phase and use the highlighter synchronously later.

In some extreme cases that you need to run Shiki completely synchronously, since v1.16, we provide a synchronous version of the core API. You can use `createHighlighterCoreSync` to create a highlighter instance synchronously.

```ts
import { createHighlighterCoreSync, createJavaScriptRegexEngine } from 'shiki/core'
import js from 'shiki/core/langs/javascript.mjs'
import nord from 'shiki/core/themes/nord.mjs'

const shiki = createHighlighterCoreSync({
themes: [nord],
langs: [js],
engine: createJavaScriptRegexEngine()
})

const html = shiki.highlight('console.log(1)', { lang: 'js', theme: 'nord' })
```

When doing so, it requires all `themes` and `langs` to be provide as plain objects. Also an explicit `engine` is required to be provided. With the new [experimental JavaScript RegExp engine](http://localhost:5173/guide/regex-engines#javascript-regexp-engine-experimental) you are able to create an engine instance synchronously as well.

The [Oniguruma Engine](http://localhost:5173/guide/regex-engines#oniguruma-engine) can only be created asynchronously, so you need to resolve the engine promise before creating the sync highlighter.

```ts
import { createHighlighterCoreSync, createWasmOnigEngine } from 'shiki/core'
import js from 'shiki/core/langs/javascript.mjs'
import nord from 'shiki/core/themes/nord.mjs'

// Load this somewhere beforehand
const engine = await createWasmOnigEngine(import('shiki/wasm'))

const shiki = createHighlighterCoreSync({
themes: [nord],
langs: [js],
engine, // if a resolved engine passed in, the rest can still be synced.
})

const html = shiki.highlight('console.log(1)', { lang: 'js', theme: 'nord' })
```
Loading