├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── astral
├── demo
│ ├── _config.ts
│ ├── deno.json
│ └── page.vto
└── mod.ts
├── cache_busting
├── demo
│ ├── _config.ts
│ ├── deno.json
│ ├── img
│ │ └── kevin-schmid-unsplash.jpg
│ ├── index.vto
│ └── page2
│ │ ├── index.njk
│ │ └── unsplash.jpg
└── mod.ts
├── check_urls
├── demo
│ ├── .gitignore
│ ├── _config.ts
│ ├── assets
│ │ └── markdown.md
│ ├── deno.json
│ ├── index.md
│ └── page2.md
└── mod.ts
├── csp
└── mod.ts
├── csrf
└── csrf.ts
├── deno.json
├── djot
├── demo
│ ├── _config.ts
│ ├── deno.json
│ ├── import_map.json
│ └── index.dj
├── deps.ts
└── mod.ts
├── fonts
├── demo
│ ├── .gitignore
│ ├── _config.ts
│ ├── deno.json
│ └── index.vto
└── mod.ts
├── google_analytics
├── deps.ts
└── mod.ts
├── hono
├── demo
│ ├── _components
│ │ └── SmallTestComponent.jsx
│ ├── _config.ts
│ ├── _includes
│ │ ├── layout.jsx
│ │ └── layout.vto
│ ├── deno.json
│ ├── hono_jsx_page_example.jsx
│ ├── index.tsx
│ ├── style.css
│ └── vento_page_example.vto
├── deps.ts
└── mod.ts
├── i18n
├── demo
│ ├── _config.ts
│ ├── _data.yml
│ ├── index-gl.njk
│ └── index.njk
├── deps.ts
└── mod.ts
├── icons
├── demo
│ ├── .gitignore
│ ├── _config.ts
│ ├── deno.json
│ └── index.vto
└── mod.ts
├── markdoc
├── demo
│ ├── _config.ts
│ ├── deno.json
│ ├── import_map.json
│ └── index.mdoc
└── mod.ts
├── notion_cms
├── demo
│ ├── _config.ts
│ ├── _includes
│ │ └── post.njk
│ ├── deno.json
│ ├── import_map.json
│ └── pages.tmpl.ts
└── mod.ts
├── order
├── demo
│ ├── .gitignore
│ ├── 1.install
│ │ ├── 1.foo.md
│ │ ├── 2.aaa.md
│ │ ├── 3.bbb
│ │ │ ├── 1.zzz.md
│ │ │ ├── 2.aaa.md
│ │ │ └── index.md
│ │ └── index.md
│ ├── 2.about.md
│ ├── _config.ts
│ ├── deno.json
│ └── index.vto
└── mod.ts
├── partytown
├── demo
│ ├── _config.ts
│ ├── _includes
│ │ └── layout.njk
│ ├── deno.json
│ ├── import_map.json
│ └── index.md
└── mod.ts
├── react
├── demo
│ ├── .gitignore
│ ├── _config.ts
│ ├── app
│ │ ├── app.tsx
│ │ ├── footer.tsx
│ │ ├── header.tsx
│ │ └── todo_item.tsx
│ ├── deno.json
│ ├── index.vto
│ └── styles.css
└── mod.ts
├── redirect
├── demo
│ ├── _config.ts
│ ├── about-me.md
│ ├── deno.json
│ └── index.md
└── mod.ts
├── router
├── demo.ts
└── mod.ts
├── sri
├── README.md
├── demo
│ ├── _config.ts
│ ├── deno.json
│ └── index.njk
└── mod.ts
├── ssx
├── demo
│ ├── .gitignore
│ ├── _config.ts
│ ├── _includes
│ │ └── layout.jsx
│ ├── deno.json
│ ├── index.jsx
│ └── other.md
└── mod.ts
├── validate_html
├── demo
│ ├── .gitignore
│ ├── _config.ts
│ ├── deno.json
│ └── index.vto
└── mod.ts
├── vue
├── demo
│ ├── _components
│ │ └── button.vue
│ ├── _config.ts
│ ├── deno.json
│ └── index.vto
├── deno.json
├── loader.ts
└── mod.ts
├── webc
├── demo
│ ├── _config.ts
│ ├── _includes
│ │ ├── layout.njk
│ │ └── webc
│ │ │ ├── no-spoiler.webc
│ │ │ └── other.webc
│ ├── deno.json
│ ├── import_map.json
│ ├── index.md
│ └── index2.md
└── mod.ts
└── wordpress
├── demo
├── _config.ts
├── _includes
│ ├── author.njk
│ ├── base.njk
│ ├── category.njk
│ ├── page.njk
│ ├── post.njk
│ └── tag.njk
├── deno.json
├── import_map.json
├── index.njk
└── pages.tmpl.ts
└── mod.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | _site
2 | _cache
3 | _bin
4 | deno.lock
5 | node_modules
6 |
7 | # Hide all .vscode dir in the repo children dir
8 | /**/.vscode
9 | # Do not ignore the top level .vscode dir
10 | !/.vscode
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.inlayHints.enabled": "off",
3 | "deno.enable": true,
4 | "deno.lint": true,
5 | "deno.unstable": true,
6 | "deno.suggest.imports.hosts": {
7 | "https://esm.sh": false,
8 | "https://deno.land": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Experimental Plugins
2 |
3 | A repo to test and experiment with plugins for Lume.
4 |
5 | If you are using any of these plugins in your projects, keep in mind that it can
6 | be removed at any time for several reasons:
7 |
8 | - It's moved to Lume main repo.
9 | - A new plugin has been created to replace it.
10 | - It's too unstable or not useful
11 |
12 | For these reasons, it's high recommended to import a specific commit, using the
13 | commit hash in the url
14 | (`https://raw.githubusercontent.com/lumeland/experimental-plugins/[hash]/[plugin]/mod.ts`)
15 |
16 | For example:
17 |
18 | ```js
19 | // Not recommended
20 | import plugin from "https://raw.githubusercontent.com/lumeland/experimental-plugins/main/plugin/mod.ts";
21 |
22 | // Recommended
23 | import plugin from "https://raw.githubusercontent.com/lumeland/experimental-plugins/69b551c39a3000b1feaf0b2d5675b4cde92141c3/plugin/mod.ts";
24 | ```
25 |
26 | ## Contributing
27 |
28 | If you use Deno plugins for vscode and you don't enable Deno on user scope. Your
29 | vscode will not able to use the correct `deno.json` file. While it is ok for
30 | building/testing, this can cause a productivity problem (ex. type checking
31 | suggestion). To avoid this, you need to:
32 |
33 | 1. create `.vscode/setting.json` in the plugin dir, that you plan to work on, by
34 | cloning `.vsode` from top repo to the plugin dir (copy&paste).
35 | 2. Then open the plugin dir in new workspace.
36 |
37 | Aside from `/.vscode` in top level repo, the rest `.vscode` are ignored by Git.
38 |
--------------------------------------------------------------------------------
/astral/demo/_config.ts:
--------------------------------------------------------------------------------
1 | import lume from "lume/mod.ts";
2 | import { Page } from "lume/core/file.ts";
3 | import astral from "../mod.ts";
4 |
5 | const site = lume();
6 | site.use(astral({
7 | callback: async (tab, page) => {
8 | await tab.waitForTimeout(1000);
9 | site.pages.push(Page.create({
10 | url: page.data.url + "foo.png",
11 | content: await tab.screenshot(),
12 | }));
13 | },
14 | }));
15 |
16 | export default site;
17 |
--------------------------------------------------------------------------------
/astral/demo/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "tasks": {
3 | "lume": "echo \"import 'lume/cli.ts'\" | deno run -A -",
4 | "build": "deno task lume",
5 | "serve": "deno task lume -s"
6 | },
7 | "imports": {
8 | "lume/": "https://deno.land/x/lume@v2.2.1/"
9 | },
10 | "compilerOptions": {
11 | "types": [
12 | "lume/types.ts"
13 | ]
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/astral/demo/page.vto:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 | graph TD;
11 | A-->B;
12 | A-->C;
13 | B-->D;
14 | C-->D;
15 |
16 |
17 | graph TD;
18 | A2-->B2;
19 | A2-->C2;
20 | B2-->D2;
21 | C2-->D2;
22 |
23 |
24 |
29 |
30 |
--------------------------------------------------------------------------------
/astral/mod.ts:
--------------------------------------------------------------------------------
1 | import { merge } from "lume/core/utils/object.ts";
2 | import { launch, Page as Tab } from "jsr:@astral/astral@0.4.2";
3 | import type { Page } from "lume/core/file.ts";
4 |
5 | import "lume/types.ts";
6 |
7 | export interface Options {
8 | /** The extensions */
9 | extensions?: string[];
10 | callback?: (tab: Tab, page: Page) => void | Promise;
11 | }
12 |
13 | export const defaults: Options = {
14 | extensions: [".html"],
15 | };
16 |
17 | export default function (userOptions: Options = {}) {
18 | const options = merge(defaults, userOptions);
19 |
20 | return (site: Lume.Site) => {
21 | site.process(options.extensions, async (pages) => {
22 | const browser = await launch();
23 |
24 | for (const page of pages) {
25 | const tab = await browser.newPage();
26 | const html = page.content;
27 | if (typeof html !== "string") {
28 | throw new Error("The content of the page must be a string");
29 | }
30 |
31 | await tab.setContent(html);
32 |
33 | // @ts-ignore _continue is a global variable
34 | // deno-lint-ignore no-window
35 | await tab.waitForFunction(() => window._continue === true);
36 |
37 | await tab.evaluate(() => {
38 | document.querySelectorAll("[astral-delete]").forEach((el) => {
39 | el.remove();
40 | });
41 | });
42 |
43 | if (options.callback) {
44 | await options.callback(tab, page);
45 | }
46 |
47 | page.content = await tab.content();
48 | tab.close();
49 | }
50 |
51 | await browser.close();
52 | });
53 | };
54 | }
55 |
--------------------------------------------------------------------------------
/cache_busting/demo/_config.ts:
--------------------------------------------------------------------------------
1 | import lume from "lume/mod.ts";
2 | import cache_busting from "../mod.ts";
3 |
4 | const site = lume();
5 |
6 | site.copy("./img/kevin-schmid-unsplash.jpg");
7 | site.copy("./page2/unsplash.jpg");
8 | site.use(cache_busting());
9 |
10 | export default site;
11 |
--------------------------------------------------------------------------------
/cache_busting/demo/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "tasks": {
3 | "lume": "echo \"import 'lume/cli.ts'\" | deno run -A -",
4 | "build": "deno task lume",
5 | "serve": "deno task lume -s"
6 | },
7 | "lock": false,
8 | "imports": {
9 | "lume/": "https://deno.land/x/lume@v3.0.1/",
10 | "lume/jsx-runtime": "https://deno.land/x/ssx@v0.1.9/jsx-runtime.ts"
11 | },
12 | "compilerOptions": {
13 | "types": [
14 | "lume/types.ts"
15 | ],
16 | "jsx": "react-jsx",
17 | "jsxImportSource": "lume"
18 | },
19 | "unstable": [
20 | "temporal"
21 | ],
22 | "lint": {
23 | "plugins": [
24 | "https://deno.land/x/lume@v3.0.1/lint.ts"
25 | ]
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/cache_busting/demo/img/kevin-schmid-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lumeland/experimental-plugins/7fe55cead5969dc0d770fc6323c53802e6c32dc9/cache_busting/demo/img/kevin-schmid-unsplash.jpg
--------------------------------------------------------------------------------
/cache_busting/demo/index.vto:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Demo
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/cache_busting/demo/page2/index.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Demo
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/cache_busting/demo/page2/unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lumeland/experimental-plugins/7fe55cead5969dc0d770fc6323c53802e6c32dc9/cache_busting/demo/page2/unsplash.jpg
--------------------------------------------------------------------------------
/cache_busting/mod.ts:
--------------------------------------------------------------------------------
1 | import { getPathAndExtension } from "lume/core/utils/path.ts";
2 | import { merge } from "lume/core/utils/object.ts";
3 | import { encodeHex } from "lume/deps/hex.ts";
4 | import { posix } from "lume/deps/path.ts";
5 | import modifyUrls from "lume/plugins/modify_urls.ts";
6 |
7 | import "lume/types.ts";
8 |
9 | export interface Options {
10 | /** Attribute used to select the elements this plugin applies to */
11 | attribute: string;
12 |
13 | /** The length of file hashes to generate */
14 | hashLength: number;
15 | }
16 |
17 | // Default options
18 | export const defaults: Options = {
19 | attribute: "hash",
20 | hashLength: 10,
21 | };
22 |
23 | const cache = new Map>();
24 |
25 | /** A plugin to add cache busting hashes to all URLs found in HTML documents. */
26 | export default function (userOptions?: Partial): Lume.Plugin {
27 | const options = merge(defaults, userOptions);
28 |
29 | return (site: Lume.Site) => {
30 | const selector = `[${options.attribute}]`;
31 |
32 | site.use(modifyUrls({ fn: replace }));
33 |
34 | async function replace(
35 | url: string | null,
36 | page: Lume.Page,
37 | element?: Element,
38 | ) {
39 | if (url && element?.matches(selector)) {
40 | return await addHash(url, page);
41 | }
42 |
43 | return "";
44 | }
45 |
46 | async function addHash(url: string, page: Lume.Page) {
47 | // Resolve relative URLs
48 | if (page.data.url && url.startsWith(".")) {
49 | url = posix.join(page.data.url, url);
50 | }
51 |
52 | // Ensure the path starts with "/"
53 | url = posix.join("/", url);
54 |
55 | if (!cache.has(url)) {
56 | cache.set(url, getHash(url));
57 | }
58 |
59 | const hash = await cache.get(url)!;
60 |
61 | const [path, ext] = getPathAndExtension(url);
62 |
63 | return `${path}-${hash}${ext}`;
64 | }
65 |
66 | async function getHash(url: string) {
67 | const content = await getFileContent(url);
68 |
69 | const contentHash = await getContentHash(content);
70 |
71 | renameFile(url, contentHash);
72 |
73 | return contentHash;
74 | }
75 |
76 | async function getFileContent(url: string): Promise {
77 | const content = await site.getContent(url, true);
78 |
79 | if (!content) {
80 | throw new Error(`Unable to find the file "${url}"`);
81 | }
82 |
83 | return content as Uint8Array;
84 | }
85 |
86 | function renameFile(url: string, hash: string) {
87 | // It's a page or static file
88 | const file = site.pages.find((page) => page.data.url === url)
89 | ?? site.files.find((file) => file.data.url === url)
90 |
91 | if (file) {
92 | const [path, ext] = getPathAndExtension(url);
93 | file.data.url = `${path}-${hash}${ext}`;
94 | return;
95 | }
96 |
97 | throw new Error(`Unable to find the file "${url}"`);
98 | }
99 |
100 | async function getContentHash(content: Uint8Array): Promise {
101 | const hashBuffer = await crypto.subtle.digest("SHA-1", content);
102 | const hash = encodeHex(new Uint8Array(hashBuffer));
103 | return hash.substring(0, options.hashLength);
104 | }
105 | };
106 | }
107 |
--------------------------------------------------------------------------------
/check_urls/demo/.gitignore:
--------------------------------------------------------------------------------
1 | _site
2 | _cache
3 | .DS_Store
4 |
--------------------------------------------------------------------------------
/check_urls/demo/_config.ts:
--------------------------------------------------------------------------------
1 | import lume from "lume/mod.ts";
2 | import resolve_urls from "lume/plugins/resolve_urls.ts";
3 | import check_urls from "../mod.ts";
4 |
5 | const site = lume();
6 | site.use(resolve_urls());
7 | site.use(check_urls());
8 | site.copy("assets");
9 |
10 | export default site;
11 |
--------------------------------------------------------------------------------
/check_urls/demo/assets/markdown.md:
--------------------------------------------------------------------------------
1 | This is a static file
2 |
--------------------------------------------------------------------------------
/check_urls/demo/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "imports": {
3 | "lume/": "https://deno.land/x/lume@v2.2.2/"
4 | },
5 | "tasks": {
6 | "lume": "echo \"import 'lume/cli.ts'\" | deno run -A -",
7 | "build": "deno task lume",
8 | "serve": "deno task lume -s"
9 | },
10 | "compilerOptions": {
11 | "types": [
12 | "lume/types.ts"
13 | ]
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/check_urls/demo/index.md:
--------------------------------------------------------------------------------
1 | # home
2 |
3 | [link to static file](./assets/markdown.md) [link to page](page2.md)
4 |
--------------------------------------------------------------------------------
/check_urls/demo/page2.md:
--------------------------------------------------------------------------------
1 | this is a page
2 |
--------------------------------------------------------------------------------
/check_urls/mod.ts:
--------------------------------------------------------------------------------
1 | // To-do: When merging the plugin into the core, move the duplicated code with modify_urls to a shared function
2 |
3 | import { merge } from "lume/core/utils/object.ts";
4 | import type Site from "lume/core/site.ts";
5 | import type { Page } from "lume/core/file.ts";
6 |
7 | export interface Options {
8 | /** The list of extensions this plugin applies to */
9 | extensions?: string[];
10 | }
11 |
12 | /** Default options */
13 | export const defaults: Options = {
14 | extensions: [".html"],
15 | };
16 |
17 | /**
18 | * This plugin checks broken links in *.html output files.
19 | */
20 | export default function (userOptions?: Options) {
21 | const options = merge(defaults, userOptions);
22 |
23 | return (site: Site) => {
24 | const url_site = site.options.location;
25 | const urls = new Set(); // Set is more performant than arrays
26 |
27 | function scan(
28 | url: string | null,
29 | page: Page,
30 | ): undefined {
31 | if (url == null) return;
32 |
33 | const full_url = new URL(url, new URL(page.data.url, url_site));
34 | if (full_url.origin != url_site.origin) {
35 | return;
36 | }
37 | full_url.hash = ""; // doesn't check hash
38 | full_url.search = ""; // doesn't check search either
39 |
40 | if (!urls.has(full_url.toString())) {
41 | console.warn(`⛓️💥 ${page.data.url} -> ${url}`);
42 | }
43 |
44 | return;
45 | }
46 |
47 | function scanSrcset(
48 | attr: string | null,
49 | page: Page,
50 | ): undefined {
51 | const srcset = attr != null ? attr.trim().split(",") : [];
52 | for (const src of srcset) {
53 | const [, url, _rest] = src.trim().match(/^(\S+)(.*)/)!;
54 | scan(url, page);
55 | }
56 | }
57 |
58 | site.process("*", (pages) => {
59 | urls.clear(); // Clear on rebuild
60 | for (const page of pages) {
61 | urls.add(new URL(page.data.url, url_site).toString()); // site.url() return the full url if the second argument is true
62 | }
63 | for (const file of site.files) {
64 | urls.add(site.url(file.outputPath, true));
65 | }
66 | });
67 |
68 | site.process(
69 | options.extensions,
70 | (pages) =>
71 | pages.forEach((page: Page) => {
72 | const { document } = page;
73 |
74 | if (!document) {
75 | return;
76 | }
77 |
78 | for (const element of document.querySelectorAll("[href]")) {
79 | scan(element.getAttribute("href"), page);
80 | }
81 |
82 | for (const element of document.querySelectorAll("[src]")) {
83 | scan(element.getAttribute("src"), page);
84 | }
85 |
86 | for (const element of document.querySelectorAll("video[poster]")) {
87 | scan(element.getAttribute("poster"), page);
88 | }
89 |
90 | for (const element of document.querySelectorAll("[srcset]")) {
91 | scanSrcset(
92 | element.getAttribute("srcset"),
93 | page,
94 | );
95 | }
96 |
97 | for (const element of document.querySelectorAll("[imagesrcset]")) {
98 | scanSrcset(
99 | element.getAttribute("imagesrcset"),
100 | page,
101 | );
102 | }
103 | }),
104 | );
105 | };
106 | }
107 |
--------------------------------------------------------------------------------
/csp/mod.ts:
--------------------------------------------------------------------------------
1 | import type { Middleware, RequestHandler } from "lume/core/server.ts";
2 | import { isPlainObject, merge } from "lume/core/utils/object.ts";
3 |
4 | const DEFAULT_MAX_AGE = 365 * 86400;
5 |
6 | interface StrictTransportSecurityOptions {
7 | /** The time, in seconds, that the browser should remember that a site is only to be accessed using HTTPS */
8 | maxAge: number;
9 |
10 | /** If this optional parameter is specified, this rule applies to all of the site's subdomains as well */
11 | includeSubDomains?: boolean;
12 |
13 | /** Enable preloading for assets (https://hstspreload.org/) */
14 | preload?: boolean;
15 | }
16 |
17 | interface ContentSecurityPolicyOptions {
18 | mergeDefaults?: boolean;
19 | directives: ContentSecurityPolicyDirectives;
20 | reportOnly?: boolean;
21 | }
22 |
23 | interface ContentSecurityPolicyDirectives {
24 | /** Defines valid sources for web workers and nested browsing contexts loaded using elements */
25 | "child-src"?: string;
26 |
27 | /** Applies to XMLHttpRequest (AJAX), WebSocket, fetch(), or EventSource */
28 | "connect-src"?: string[];
29 |
30 | /** Defines the default policy for fetching resources */
31 | "default-src"?: string[];
32 |
33 | /** Defines valid sources of font resources (loaded via @font-face) */
34 | "font-src"?: string[];
35 |
36 | /** Defines valid sources for loading frames */
37 | "frame-src"?: string[];
38 |
39 | /** Defines valid sources of images */
40 | "img-src"?: string[];
41 |
42 | /** Restricts the URLs that application manifests can be loaded */
43 | "manifest-src"?: string[];
44 |
45 | /** Defines valid sources of audio and video, eg HTML5