├── .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