├── .editorconfig
├── .gitattributes
├── .github
├── FUNDING.yml
└── workflows
│ ├── codeql.yml
│ ├── docs.yml
│ ├── lint.yml
│ ├── stale.yml
│ └── test.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── docs
├── .gitignore
├── .vitepress
│ ├── config.ts
│ ├── escape-code.ts
│ ├── md
│ │ ├── index.ts
│ │ ├── md-link.ts
│ │ └── md-replacer.ts
│ └── theme
│ │ ├── index.ts
│ │ └── style.css
├── guide
│ ├── asset-import.md
│ ├── getting-started.md
│ ├── gpu-acceleration.md
│ ├── index.md
│ ├── integrations
│ │ ├── astro.md
│ │ ├── nuxt.md
│ │ ├── remix.md
│ │ ├── solid.md
│ │ ├── svelte.md
│ │ ├── vite.md
│ │ └── vitepress.md
│ └── plugins.md
├── index.md
├── package.json
├── public
│ ├── favicon.svg
│ ├── fonts
│ │ ├── inter.woff2
│ │ └── jb.woff2
│ ├── img
│ │ ├── hero.blend
│ │ ├── hero.webp
│ │ └── og.jpg
│ ├── imgit
│ │ ├── covers.json
│ │ ├── encoded
│ │ │ ├── i.gyazo.com-1bd7705bf63b4fa179e45a7b4445d34e.png@cover.avif
│ │ │ ├── i.gyazo.com-1bd7705bf63b4fa179e45a7b4445d34e.png@main.avif
│ │ │ ├── i.gyazo.com-2f5c124d0bd7c96a91a3bc19a4365850.mp4@cover.avif
│ │ │ ├── i.gyazo.com-2f5c124d0bd7c96a91a3bc19a4365850.mp4@main.mp4
│ │ │ ├── i.gyazo.com-4122593cabf787177589cec34e94263c.png@cover.avif
│ │ │ ├── i.gyazo.com-4122593cabf787177589cec34e94263c.png@main.avif
│ │ │ ├── i.gyazo.com-57489480e03593abb47d78c5e1374aa7.png@cover.avif
│ │ │ ├── i.gyazo.com-57489480e03593abb47d78c5e1374aa7.png@main.avif
│ │ │ ├── i.gyazo.com-845ed7b1a635187b00f93a3e7f2730ae.mp4@cover.avif
│ │ │ ├── i.gyazo.com-845ed7b1a635187b00f93a3e7f2730ae.mp4@main.mp4
│ │ │ ├── i.gyazo.com-b2f45680247820c398682d7150fca566.mp4@cover.avif
│ │ │ ├── i.gyazo.com-b2f45680247820c398682d7150fca566.mp4@main.mp4
│ │ │ ├── i.gyazo.com-d1f5eb93e57799d1a3cd2abdf438c040.png@cover.avif
│ │ │ ├── i.gyazo.com-d1f5eb93e57799d1a3cd2abdf438c040.png@main.avif
│ │ │ ├── i.ytimg.com-vi_webp-arbuYnJoLtU-maxresdefault.webp@cover.avif
│ │ │ ├── i.ytimg.com-vi_webp-arbuYnJoLtU-maxresdefault.webp@main.avif
│ │ │ ├── svg-imgit-nutshell-dark.png@cover.avif
│ │ │ ├── svg-imgit-nutshell-dark.png@dense.avif
│ │ │ ├── svg-imgit-nutshell-dark.png@main.avif
│ │ │ ├── svg-imgit-nutshell-light.png@cover.avif
│ │ │ ├── svg-imgit-nutshell-light.png@dense.avif
│ │ │ └── svg-imgit-nutshell-light.png@main.avif
│ │ ├── fetched
│ │ │ ├── i.gyazo.com-1bd7705bf63b4fa179e45a7b4445d34e.png
│ │ │ ├── i.gyazo.com-2f5c124d0bd7c96a91a3bc19a4365850.mp4
│ │ │ ├── i.gyazo.com-4122593cabf787177589cec34e94263c.png
│ │ │ ├── i.gyazo.com-57489480e03593abb47d78c5e1374aa7.png
│ │ │ ├── i.gyazo.com-845ed7b1a635187b00f93a3e7f2730ae.mp4
│ │ │ ├── i.gyazo.com-b2f45680247820c398682d7150fca566.mp4
│ │ │ ├── i.gyazo.com-d1f5eb93e57799d1a3cd2abdf438c040.png
│ │ │ └── i.ytimg.com-vi_webp-arbuYnJoLtU-maxresdefault.webp
│ │ ├── probes.json
│ │ ├── sizes.json
│ │ ├── specs.json
│ │ └── youtube.json
│ └── svg
│ │ ├── imgit-nutshell-dark.png
│ │ ├── imgit-nutshell-light.png
│ │ └── imgit-nutshell.excalidraw
├── scripts
│ └── api.sh
└── tsconfig.json
├── package.json
├── samples
├── .gitignore
├── assets
│ ├── README.md
│ ├── apng.apng
│ ├── art-square.jpg
│ ├── art-wide.jpg
│ ├── avi.avi
│ ├── avif.avif
│ ├── bmp.bmp
│ ├── gif.gif
│ ├── jpg.jpg
│ ├── mkv.mkv
│ ├── mov.mov
│ ├── mp4.mp4
│ ├── png.png
│ ├── psd.psd
│ ├── svg.svg
│ ├── tga.tga
│ ├── tif.tif
│ ├── tiff.tiff
│ ├── webm.webm
│ └── webp.webp
├── astro
│ ├── README.md
│ ├── astro.config.mts
│ ├── package.json
│ ├── src
│ │ ├── env.d.ts
│ │ └── pages
│ │ │ ├── import.astro
│ │ │ └── index.astro
│ └── tsconfig.json
├── minimal
│ ├── README.md
│ ├── build.ts
│ └── index.html
├── nuxt
│ ├── README.md
│ ├── app.vue
│ ├── nuxt.config.ts
│ ├── package.json
│ └── tsconfig.json
├── remix
│ ├── README.md
│ ├── app
│ │ ├── app.css
│ │ └── root.tsx
│ ├── package.json
│ ├── tsconfig.json
│ └── vite.config.ts
├── solid
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── app.css
│ │ ├── app.tsx
│ │ ├── entry-client.tsx
│ │ └── entry-server.tsx
│ ├── tsconfig.json
│ └── vite.config.ts
├── svelte
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── app.html
│ │ └── routes
│ │ │ └── +page.svelte
│ ├── tsconfig.json
│ └── vite.config.ts
├── vite
│ ├── README.md
│ ├── index.html
│ ├── package.json
│ ├── tsconfig.json
│ └── vite.config.ts
└── vitepress
│ ├── .vitepress
│ ├── config.mts
│ └── theme
│ │ ├── index.mts
│ │ └── styles.css
│ ├── README.md
│ ├── index.md
│ ├── package.json
│ └── tsconfig.json
├── scripts
└── build.sh
├── src
├── client.d.ts
├── client
│ ├── index.ts
│ ├── intersection.ts
│ ├── mutation.ts
│ └── styles.css
├── plugin
│ ├── astro.ts
│ ├── svg.ts
│ ├── vite.ts
│ └── youtube
│ │ ├── client.ts
│ │ ├── index.ts
│ │ ├── server.ts
│ │ └── styles.css
├── server
│ ├── asset.ts
│ ├── cache.ts
│ ├── common.ts
│ ├── config
│ │ ├── defaults.ts
│ │ ├── index.ts
│ │ ├── options.ts
│ │ └── plugin.ts
│ ├── context.ts
│ ├── ffmpeg
│ │ ├── ffmpeg.ts
│ │ ├── ffprobe.ts
│ │ └── index.ts
│ ├── import.ts
│ ├── index.ts
│ ├── platform
│ │ ├── bun.ts
│ │ ├── deno.ts
│ │ ├── index.ts
│ │ ├── node.ts
│ │ └── platform.ts
│ └── transform
│ │ ├── 1-capture.ts
│ │ ├── 2-resolve.ts
│ │ ├── 3-fetch.ts
│ │ ├── 4-probe.ts
│ │ ├── 5-encode.ts
│ │ ├── 6-build.ts
│ │ ├── 7-rewrite.ts
│ │ └── index.ts
└── tsconfig.json
└── test
├── client
├── client.spec.ts
├── common.ts
├── intersection.spec.ts
├── mutation.spec.ts
└── youtube.spec.ts
├── server
├── 1-capture.spec.ts
├── 2-resolve.spec.ts
├── 3-fetch.spec.ts
├── 4-probe.spec.ts
├── 5-encode.spec.ts
├── 6-build.spec.ts
├── 7-rewrite.spec.ts
├── astro.spec.ts
├── common.ts
├── import.spec.ts
├── server.spec.ts
├── svg.spec.ts
├── vite.spec.ts
└── youtube.spec.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 | indent_style = space
9 | tab_width = 4
10 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: elringus
2 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: codeql
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | analyze:
11 | name: analyze
12 | runs-on: ubuntu-latest
13 | permissions:
14 | actions: read
15 | contents: read
16 | security-events: write
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | language: [ 'javascript' ]
21 | steps:
22 | - name: checkout
23 | uses: actions/checkout@v4
24 | - uses: actions/setup-node@v3
25 | with:
26 | node-version: 20
27 | - name: initialize codeql
28 | uses: github/codeql-action/init@v2
29 | with:
30 | languages: ${{ matrix.language }}
31 | - name: build
32 | run: |
33 | npm install
34 | bash scripts/build.sh
35 | - name: analyze
36 | uses: github/codeql-action/analyze@v2
37 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: docs
2 |
3 | on:
4 | workflow_dispatch: { }
5 | push:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | deploy:
11 | runs-on: ubuntu-latest
12 | permissions:
13 | pages: write
14 | id-token: write
15 | environment:
16 | name: github-pages
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: fetch tags
20 | run: git fetch --prune --unshallow --tags
21 | - uses: actions/setup-node@v4
22 | - name: build
23 | run: |
24 | cd docs
25 | npm install
26 | npm run docs:api
27 | npm run docs:build
28 | - uses: actions/configure-pages@v5
29 | - uses: actions/upload-pages-artifact@v3
30 | with:
31 | path: docs/.vitepress/dist
32 | - uses: actions/deploy-pages@v4
33 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: lint
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | editor:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: editorconfig
15 | run: |
16 | docker run --rm --volume=$PWD:/check mstruebing/editorconfig-checker ec --exclude "\.git|[\\/]public[\\/]|[\\/]assets[\\/]"
17 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: stale
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '45 3 * * *'
7 |
8 | jobs:
9 | scan:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/stale@v9
13 | id: stale
14 | with:
15 | stale-issue-label: stale
16 | stale-pr-label: stale
17 | stale-issue-message: 'This issue is stale because it has been open 14 days with no activity. It will be automatically closed in 7 days.'
18 | stale-pr-message: 'This pull request is stale because it has been open 14 days with no activity. It will be automatically closed in 7 days.'
19 | days-before-stale: 14
20 | days-before-close: 7
21 | exempt-issue-labels: 'bug,enhancement'
22 | exempt-pr-labels: 'bug,enhancement'
23 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | cover:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: actions/setup-node@v3
15 | with:
16 | node-version: 20
17 | - name: cover
18 | run: |
19 | npm install
20 | npm run cover
21 | - name: upload report
22 | uses: codecov/codecov-action@v3
23 | with:
24 | fail_ci_if_error: true
25 | env:
26 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vs
2 | .idea
3 |
4 | dist
5 | coverage
6 | node_modules
7 | package-lock.json
8 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Artyom Sovetnikov (Elringus)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | # Convert media links to optimized HTML
16 |
17 | Images, video and YouTube: fetch, encode, scale, lazyload – for best UX and [Web Vitals](https://web.dev/vitals) — imgit is a JavaScript package and set of plugins for popular web frameworks to enhance user experience when interacting with media-heavy websites, such as blogs, landings, portfolios and documentation sites.
18 |
19 | ## Features
20 |
21 | ✨ Builds optimized HTML for media URLs, markdown or JSX tags
22 |
23 | ⚡ Encodes to AV1/AVIF utilizing GPU acceleration
24 |
25 | ♻️ Supports JPG, A/PNG, GIF, WEBM/P, AVI, MKV, TIFF, PSD and more
26 |
27 | 🌊 Generates tiny blurred covers cross-faded into HD source on lazy-load
28 |
29 | 📐 Scales the content while preserving HD original for high-DPI displays
30 |
31 | 🌐 Fetches sources from remote URLs, uploads optimized versions to CDN
32 |
33 | 🗺️ Plugs into Astro, Svelte, VitePress and more; runs on Node, Deno and Bun
34 |
35 | ## 🎬 Get Started
36 |
37 | http://imgit.dev/guide
38 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | cache
2 | api
3 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, DefaultTheme } from "vitepress";
2 | import md from "./md";
3 | import escapeCode from "./escape-code";
4 | import imgit from "imgit/vite";
5 | import svg from "imgit/svg";
6 | import youtube from "imgit/youtube";
7 |
8 | // https://vitepress.dev/reference/site-config
9 | export default defineConfig({
10 | title: "imgit",
11 | titleTemplate: ":title • imgit",
12 | description: "JavaScript utility to optimize web media content.",
13 | appearance: "dark",
14 | cleanUrls: true,
15 | lastUpdated: true,
16 | markdown: md,
17 | vite: { plugins: [imgit({ width: 688, plugins: [svg(), youtube(), escapeCode] })] },
18 | head: [
19 | ["link", { rel: "icon", href: "/favicon.svg" }],
20 | ["link", { rel: "preload", href: "/fonts/inter.woff2", as: "font", type: "font/woff2", crossorigin: "" }],
21 | ["link", { rel: "preload", href: "/fonts/jb.woff2", as: "font", type: "font/woff2", crossorigin: "" }],
22 | ["meta", { name: "theme-color", content: "#ee3248" }],
23 | ["meta", { name: "og:image", content: "/img/og.jpg" }],
24 | ["meta", { name: "twitter:card", content: "summary_large_image" }]
25 | ],
26 | themeConfig: {
27 | logo: { src: "/favicon.svg" },
28 | logoLink: "/",
29 | socialLinks: [{ icon: "github", link: "https://github.com/elringus/imgit" }],
30 | search: { provider: "local", options: { detailedView: true } },
31 | lastUpdated: { text: "Updated", formatOptions: { dateStyle: "medium" } },
32 | sidebarMenuLabel: "Menu",
33 | darkModeSwitchLabel: "Appearance",
34 | returnToTopLabel: "Return to top",
35 | outline: { label: "On this page", level: [2, 3] },
36 | docFooter: { prev: "Previous page", next: "Next page" },
37 | nav: [
38 | { text: "Guide", link: "/guide/", activeMatch: "/guide/" },
39 | { text: "Reference", link: "/api/", activeMatch: "/api/" },
40 | {
41 | text: `v${(await import("./../../package.json")).version}`, items: [
42 | { text: "Changes", link: "https://github.com/elringus/imgit/releases/latest" },
43 | { text: "Contribute", link: "https://github.com/elringus/imgit/labels/help%20wanted" }
44 | ]
45 | }
46 | ],
47 | editLink: {
48 | pattern: "https://github.com/elringus/imgit/edit/main/docs/:path",
49 | text: "Edit this page on GitHub"
50 | },
51 | sidebar: {
52 | "/guide/": [
53 | {
54 | text: "Guide",
55 | items: [
56 | { text: "Introduction", link: "/guide/" },
57 | { text: "Getting Started", link: "/guide/getting-started" },
58 | { text: "Asset Import", link: "/guide/asset-import" },
59 | { text: "GPU Acceleration", link: "/guide/gpu-acceleration" },
60 | { text: "Plugins", link: "/guide/plugins" }
61 | ]
62 | },
63 | {
64 | text: "Integrations",
65 | items: [
66 | { text: "Vite", link: "/guide/integrations/vite" },
67 | { text: "Astro", link: "/guide/integrations/astro" },
68 | { text: "Nuxt", link: "/guide/integrations/nuxt" },
69 | { text: "Remix", link: "/guide/integrations/remix" },
70 | { text: "SolidStart", link: "/guide/integrations/solid" },
71 | { text: "SvelteKit", link: "/guide/integrations/svelte" },
72 | { text: "VitePress", link: "/guide/integrations/vitepress" }
73 | ]
74 | }
75 | ],
76 | "/api/": await getApiSidebar()
77 | }
78 | },
79 | sitemap: { hostname: "https://imgit.dev" }
80 | });
81 |
82 | async function getApiSidebar(): Promise {
83 | const items = (await import("./../api/typedoc-sidebar.json")).default;
84 | const server = items.find(i => i.text === "server");
85 | const client = items.find(i => i.text === "client");
86 | const other = items.filter(i => i !== server && i !== client);
87 | return [{ text: "Reference", items: [server, client, ...other] }];
88 | }
89 |
--------------------------------------------------------------------------------
/docs/.vitepress/escape-code.ts:
--------------------------------------------------------------------------------
1 | import { Plugin, CapturedAsset, stages } from "imgit/server";
2 |
3 | type Range = { start: number; end: number; };
4 |
5 | export default { capture } satisfies Plugin;
6 |
7 | // Remove captures inside Markdown code blocks (```code```).
8 | function capture(content: string, assets: CapturedAsset[]): boolean {
9 | stages.capture.assets(content, assets);
10 | if (assets.length === 0) return true;
11 | const ranges = findCodeRanges(content);
12 | if (ranges.length > 0)
13 | assets.splice(0, assets.length, ...assets.filter(a => !isInCodeBlock(a, ranges)));
14 | return true;
15 | }
16 |
17 | function findCodeRanges(content: string): Range[] {
18 | const ranges = new Array();
19 | let tickCount = 0;
20 | let openIndex = -1;
21 | for (let i = 0; i < content.length; i++)
22 | if (content[i] === "`") handleTick(i);
23 | else tickCount = 0;
24 | return ranges;
25 |
26 | function handleTick(index: number): void {
27 | if (++tickCount < 3) return;
28 | if (openIndex === -1) openRange(index);
29 | else closeRange(index);
30 | }
31 |
32 | function openRange(index: number): void {
33 | openIndex = index;
34 | tickCount = 0;
35 | }
36 |
37 | function closeRange(index: number): void {
38 | ranges.push({ start: openIndex, end: index });
39 | openIndex = -1;
40 | tickCount = 0;
41 | }
42 | }
43 |
44 | function isInCodeBlock(asset: CapturedAsset, ranges: Range[]): boolean {
45 | for (const range of ranges)
46 | if (asset.syntax.index >= range.start &&
47 | (asset.syntax.index + asset.syntax.text.length) <= range.end)
48 | return true;
49 | return false;
50 | }
51 |
--------------------------------------------------------------------------------
/docs/.vitepress/md/index.ts:
--------------------------------------------------------------------------------
1 | import { MarkdownOptions, MarkdownRenderer } from "vitepress";
2 | import { AppendIconToExternalLinks } from "./md-link";
3 |
4 | export default {
5 | config: installPlugins,
6 | attrs: { disable: true } // https://github.com/vuejs/vitepress/issues/2440
7 | } satisfies MarkdownOptions;
8 |
9 | function installPlugins(md: MarkdownRenderer) {
10 | md.use(AppendIconToExternalLinks);
11 | }
12 |
--------------------------------------------------------------------------------
/docs/.vitepress/md/md-link.ts:
--------------------------------------------------------------------------------
1 | import type { MarkdownRenderer } from "vitepress";
2 | import type { RenderRule } from "markdown-it/lib/renderer";
3 |
4 | export function AppendIconToExternalLinks(md: MarkdownRenderer) {
5 | const renderToken: RenderRule = (tokens, idx, options, env, self) => self.renderToken(tokens, idx, options);
6 | const defaultLinkOpenRenderer = md.renderer.rules.link_open || renderToken;
7 | const defaultLinkCloseRenderer = md.renderer.rules.link_close || renderToken;
8 | let externalLinkOpen = false;
9 |
10 | md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
11 | const token = tokens[idx];
12 | const href = token.attrGet("href");
13 |
14 | if (href) {
15 | token.attrJoin("class", "vp-link");
16 | if (/^((ht|f)tps?):\/\/?/.test(href))
17 | externalLinkOpen = true;
18 | }
19 |
20 | return defaultLinkOpenRenderer(tokens, idx, options, env, self);
21 | };
22 |
23 | md.renderer.rules.link_close = (tokens, idx, options, env, self) => {
24 | if (externalLinkOpen) {
25 | externalLinkOpen = false;
26 | return ` ↗${self.renderToken(tokens, idx, options)}`;
27 | }
28 | return defaultLinkCloseRenderer(tokens, idx, options, env, self);
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/docs/.vitepress/md/md-replacer.ts:
--------------------------------------------------------------------------------
1 | // Based on https://github.com/rlidwka/markdown-it-regexp.
2 |
3 | import { inherits } from "node:util";
4 | import { MarkdownEnv, MarkdownRenderer } from "vitepress";
5 |
6 | let instanceId = 0;
7 |
8 | export function Replacer(regexp: RegExp, replace: (match: string[], env: MarkdownEnv) => string) {
9 | let self: any = (md: any) => self.init(md);
10 | self.__proto__ = Replacer.prototype;
11 | self.regexp = new RegExp("^" + regexp.source, regexp.flags);
12 | self.replace = replace;
13 | self.id = `md-replacer-${instanceId}`;
14 | instanceId++;
15 | return self;
16 | }
17 |
18 | inherits(Replacer, Function);
19 |
20 | Replacer.prototype.init = function (md: MarkdownRenderer) {
21 | md.inline.ruler.push(this.id, this.parse.bind(this));
22 | md.renderer.rules[this.id] = this.render.bind(this);
23 | };
24 |
25 | Replacer.prototype.parse = function (state: any, silent: any) {
26 | let match = this.regexp.exec(state.src.slice(state.pos));
27 | if (!match) return false;
28 |
29 | state.pos += match[0].length;
30 | if (silent) return true;
31 |
32 | let token = state.push(this.id, "", 0);
33 | token.meta = { match: match };
34 | return true;
35 | };
36 |
37 | Replacer.prototype.render = function (tokens: any, id: any, options: any, env: MarkdownEnv) {
38 | return this.replace(tokens[id].meta.match, env);
39 | };
40 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | import DefaultTheme from "vitepress/theme-without-fonts";
2 | import "./style.css";
3 |
4 | // Have to import client assets manually due to vitepress
5 | // bug: https://github.com/vuejs/vitepress/issues/3314
6 | import "imgit/styles";
7 | import "imgit/styles/youtube";
8 | import "imgit/client";
9 | import "imgit/client/youtube";
10 |
11 | // https://vitepress.dev/guide/extending-default-theme
12 | export default { extends: { Layout: DefaultTheme.Layout } };
13 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/style.css:
--------------------------------------------------------------------------------
1 | /** https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */
2 |
3 | @font-face {
4 | font-family: "Inter";
5 | font-style: normal;
6 | font-weight: 100 900;
7 | font-display: swap;
8 | /* noinspection CssUnknownTarget */
9 | src: url(/fonts/inter.woff2) format("woff2");
10 | }
11 |
12 | @font-face {
13 | font-family: "JetBrains";
14 | font-style: normal;
15 | font-weight: 100 900;
16 | font-display: swap;
17 | /* noinspection CssUnknownTarget */
18 | src: url(/fonts/jb.woff2) format("woff2");
19 | }
20 |
21 | :root {
22 | --vp-font-family-base: "Inter", sans-serif;
23 | --vp-font-family-mono: "JetBrains", monospace;
24 | font-optical-sizing: auto;
25 |
26 | /* Main highlight/link color (light mode). */
27 | --vp-c-brand-1: #d22c40;
28 | /* Link hover color (light mode). */
29 | --vp-c-brand-2: #be2033;
30 | }
31 |
32 | .dark {
33 | /* Main highlight/link color (dark mode). */
34 | --vp-c-brand-1: #ee3248;
35 | /* Link hover color (dark mode). */
36 | --vp-c-brand-2: #f35d44;
37 | }
38 |
39 | .dark .dark-only,
40 | .light-only {
41 | display: flex !important;
42 | }
43 |
44 | .dark .light-only,
45 | .dark-only {
46 | display: none !important;
47 | }
48 |
49 | .attr {
50 | display: block;
51 | text-align: right;
52 | font-size: 11px;
53 | opacity: 0.65;
54 | transition: opacity 0.25s;
55 | font-family: "JetBrains", monospace;
56 | user-select: none;
57 | }
58 |
59 | .attr:hover {
60 | opacity: 1;
61 | }
62 |
63 | pre {
64 | font-variant-ligatures: none;
65 | }
66 |
67 | .vp-doc a {
68 | text-underline-position: from-font;
69 | }
70 |
71 | .external-link-icon {
72 | margin-left: -1px;
73 | margin-right: 2px;
74 | }
75 |
76 | details summary {
77 | font-weight: 500;
78 | color: var(--vp-c-brand-1);
79 | text-decoration: underline;
80 | text-underline-offset: 2px;
81 | transition: color 0.25s, opacity 0.25s;
82 | text-underline-position: from-font;
83 | cursor: pointer;
84 | }
85 |
86 | details summary:hover {
87 | color: var(--vp-c-brand-2);
88 | }
89 |
90 | .vp-doc th, .vp-doc td, .vp-doc tr {
91 | border: inherit !important;
92 | }
93 |
94 | table {
95 | --border: 1px solid var(--vp-c-divider);
96 | width: fit-content;
97 | border-radius: 8px;
98 | border-spacing: 0;
99 | border-collapse: separate !important;
100 | border: var(--border) !important;
101 | overflow: hidden;
102 | }
103 |
104 | table th:not(:last-child),
105 | table td:not(:last-child) {
106 | border-right: var(--border) !important;
107 | }
108 |
109 | table > thead > tr:not(:last-child) > th,
110 | table > thead > tr:not(:last-child) > td,
111 | table > tbody > tr:not(:last-child) > th,
112 | table > tbody > tr:not(:last-child) > td,
113 | table > tfoot > tr:not(:last-child) > th,
114 | table > tfoot > tr:not(:last-child) > td,
115 | table > tr:not(:last-child) > td,
116 | table > tr:not(:last-child) > th,
117 | table > thead:not(:last-child),
118 | table > tbody:not(:last-child),
119 | table > tfoot:not(:last-child) {
120 | border-bottom: var(--border) !important;
121 | }
122 |
123 | /* Stretch YouTube embeds with small thumbnails. */
124 | .imgit-youtube {
125 | width: 100% !important;
126 | }
127 |
128 | [data-imgit-container],
129 | [data-imgit-container] img,
130 | [data-imgit-container] video {
131 | border-radius: 8px;
132 | }
133 |
134 | /**
135 | * Navbar blur effect
136 | * -------------------------------------------------------------------------- */
137 |
138 | /* remove default opaque bg from navbar */
139 | .content-body {
140 | background: none !important;
141 | }
142 |
143 | /* remove opacity gradient under navbar */
144 | .curtain,
145 | .aside-curtain {
146 | display: none !important;
147 | }
148 |
149 | /* a hack for logo title to keep sidebar bg color */
150 | @media (min-width: 960px) {
151 | .VPNavBar.has-sidebar div.title {
152 | background: var(--vp-sidebar-bg-color) !important;
153 | }
154 | }
155 |
156 | /* a bit of tint to make navbar not as transparent */
157 | .VPNavBar:not(.top) {
158 | background: rgba(255, 255, 255, 0.75) !important;
159 | }
160 |
161 | .dark .VPNavBar:not(.top) {
162 | background: rgba(30, 30, 32, 0.5) !important;
163 | }
164 |
165 | /* the blur effect */
166 | .VPNavBar:not(.top)::after {
167 | content: "";
168 | position: absolute;
169 | top: 0;
170 | left: 0;
171 | right: 0;
172 | bottom: 0;
173 | backdrop-filter: saturate(180%) blur(5px);
174 | z-index: -1;
175 | }
176 |
177 | .VPNavBar.top::after {
178 | opacity: 0;
179 | }
180 |
181 | .VPNavBar.top {
182 | background-color: transparent;
183 | }
184 |
185 | .VPNavBar {
186 | transition: border-bottom-color 0.25s, background-color 0.25s, opacity 0.25s;
187 | }
188 |
--------------------------------------------------------------------------------
/docs/guide/asset-import.md:
--------------------------------------------------------------------------------
1 | # Asset Import
2 |
3 | By default, imgit is set up to detect and transform Markdown syntax in the source content. This works best for simple documentation and blog websites, but may not be flexible enough for more complex apps authored with frameworks like React.
4 |
5 | To better fit component-based apps, imgit allows importing media assets with `import` statement to manually author the desired HTML.
6 |
7 | Use `imgit:` namespace when importing a media asset to make imgit optimize it and return sources of the generated assets. For example, consider following [Astro](https://astro.build) page:
8 |
9 | ```astro
10 | ---
11 | import psd from "imgit:https://example.com/photo.psd";
12 | import mkv from "imgit:/assets/video.mkv";
13 | ---
14 |
15 |
18 |
19 |
23 | ```
24 |
25 | Imported asset returns following default export:
26 |
27 | ```ts
28 | type AssetImport = {
29 | content: {
30 | encoded: string,
31 | dense?: string,
32 | cover?: string,
33 | safe?: string
34 | },
35 | info: {
36 | type: string,
37 | height: number,
38 | width: number,
39 | alpha: boolean
40 | }
41 | };
42 | ```
43 |
44 | — where `content` are the sources of the generated optimized files, which you can assign to the various `src` attributes of the built HTML. Additional `info` object contains metadata describing the imported asset, such its dimensions and MIME type, which may be helpful when building the host component.
45 |
46 | ::: tip
47 | When using TypeScript, add `/// ` to a `.d.ts` file anywhere under project source directory to correctly resolve virtual asset imports.
48 | :::
49 |
--------------------------------------------------------------------------------
/docs/guide/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | Make sure [ffmpeg](https://ffmpeg.org) version 6.0 or later is available in system path. You can either [build from source](https://trac.ffmpeg.org/wiki/CompilationGuide) or install from a package manager:
4 |
5 | ::: code-group
6 |
7 | ```sh [Linux]
8 | apt install ffmpeg
9 | ```
10 |
11 | ```sh [Mac]
12 | brew install ffmpeg
13 | ```
14 |
15 | ```sh [Windows]
16 | choco install ffmpeg
17 | ```
18 |
19 | :::
20 |
21 | ::: tip
22 | It's possible to swap ffmpeg with an alternative solution (eg, remote encoding service) via probe and encode hooks, allowing to use imgit in constraint environments, such as edge runtimes.
23 | :::
24 |
25 | Install imgit as a dev dependency from NPM:
26 |
27 | ::: code-group
28 |
29 | ```sh [npm]
30 | npm install -D imgit
31 | ```
32 |
33 | ```sh [yarn]
34 | yarn add -D imgit
35 | ```
36 |
37 | ```sh [pnpm]
38 | pnpm add -D imgit
39 | ```
40 |
41 | ```sh [bun]
42 | bun add -D imgit
43 | ```
44 |
45 | :::
46 |
47 | When using any of the supported web frameworks continue on the dedicated page:
48 |
49 | - [Astro](/guide/integrations/astro)
50 | - [Nuxt](/guide/integrations/nuxt)
51 | - [Remix](/guide/integrations/remix)
52 | - [SolidStart](/guide/integrations/solid)
53 | - [SvelteKit](/guide/integrations/svelte)
54 | - [VitePress](/guide/integrations/vitepress)
55 |
56 | In case your framework is not on the list, but supports Vite plugins, continue on [Vite](/guide/integrations/vite).
57 |
58 | Otherwise, use imgit directly to transform source documents. For example, giving following `./index.html` file:
59 |
60 | ```html
61 |
62 |
63 |
64 |
65 |
67 |
68 |
69 |
70 |
71 | 
72 | 
73 | 
74 |
75 |
76 |
77 |
78 |
79 | ```
80 |
81 | Run following script:
82 |
83 | ```js
84 | import { boot, transform, exit } from "imgit/server";
85 | import fs from "node:fs/promises";
86 |
87 | // Configure imgit server. In this case we're setting width threshold
88 | // to 800px, so that when content is larger it'll be scaled down,
89 | // while high-res original will still be shown on high-dpi displays.
90 | await boot({ width: 800 });
91 |
92 | // Read sample HTML document with images and video referenced
93 | // via markdown image tags: . The format can be changed
94 | // in boot config, for example to capture custom JSX tags instead.
95 | const input = await fs.readFile("./index.html", { encoding: "utf8" });
96 |
97 | // Run the imgit transformations over sample HTML content.
98 | // This will capture images and video syntax, fetch the remote files,
99 | // encode them to AV1/AVIF, generate covers, dense and safe variants
100 | // when necessary, serve generated files (in this minimal case we just
101 | // write them to 'public' directory; usually you'd upload to a CDN) and
102 | // return transformed content where captured syntax is replaced with
103 | // and