Changes
29 changed files (+1192/-76)
-
-
@@ -14,4 +14,14 @@ runs-on: ubuntu-lateststeps: - name: Checkout repository uses: actions/checkout@v3 # TODO: Re-implement documentation website - name: Setup Deno uses: denoland/setup-deno@v1 with: deno-version: v1.26.x - name: Build doc site run: deno task docs_build https://pocka.github.io/slack-message-parser/ - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs/site/build
-
-
-
@@ -4,21 +4,25 @@ "strict": true}, "test": { "files": { "exclude": ["npm", "coverage"] "exclude": ["npm", "coverage", "docs/build"] } }, "lint": { "files": { "exclude": ["npm", "coverage"] "exclude": ["npm", "coverage", "docs/build"] } }, "fmt": { "files": { "exclude": ["npm", "coverage"] "exclude": ["npm", "coverage", "docs/build"] }, "options": { "proseWrap": "preserve" } }, "importMap": "import_map.json" "importMap": "import_map.json", "tasks": { "docs_build": "deno run --allow-read=. --allow-write=./docs/site/build --allow-net=unpkg.com,deno.land ./docs/site/build.tsx", "docs_serve": "deno run --allow-net --allow-read=. https://deno.land/std@0.161.0/http/file_server.ts ./docs/site/build" } }
-
-
docs/.gitignore (new)
-
@@ -0,0 +1,1 @@/site/build
-
-
docs/.vuepress/config.js (deleted)
-
@@ -1,17 +0,0 @@module.exports = { title: "slack-message-parser", description: "Documentation for slack-message-parser", // Assume hosted on GitHub Pages. base: "/slack-message-parser/", themeConfig: { nav: [ { text: "Guide", link: "/" }, { text: "API", link: "/api/" }, { text: "GitHub", link: "https://github.com/pocka/slack-message-parser", }, ], sidebar: ["/", "/api/"], }, };
-
-
docs/README.md (deleted)
-
@@ -1,51 +0,0 @@# Guide slack-message-parser is a JavaScript library which parses the message returned by Slack API and give you a tree object (imagine AST). You could use the result tree to create a message viewer (HTML, React component, etc...) or whatever you want :) ## Installation The package is available as `slack-message-parser` on npm. ```sh yarn add slack-message-parser # npm i --save slack-message-parser ``` ## Quick Start Just parse the message! ```ts import slackMessageParser from "slack-message-parser"; const tree = slackMessageParser("Slack *message* ~to~ _parse_"); console.dir(tree); ``` ## Supported Message Features - Plain text - Links - Channels (`#channel`) - User (`@someone`) - URL (`https://foo.bar`, `mailto:foo@bar`) - Commands (Represented as `<!foo>`, more detail [here](https://api.slack.com/docs/message-formatting)) - Emojis :heart_eyes: - Code block - Inline code (<code>\`foo\`</code>) - Italic (`_foo_`) - Bold (`*foo*`) - Strikethrough (`~foo~`) - Quotes (`> foo`) ## Examples ### Plain HTML with TypeScript <iframe src="https://codesandbox.io/embed/gracious-rgb-kmqbu?fontsize=14&module=%2Fsrc%2Findex.ts&view=editor" title="slack-message-parser--ts-vanilla" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe> ### React with TypeScript <iframe src="https://codesandbox.io/embed/condescending-heyrovsky-y5jw3?fontsize=14&module=%2Fsrc%2FSlackMessage.tsx" title="slack-message-parser--react-ts" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
-
-
-
@@ -1,7 +1,5 @@# API [[toc]] ## `function parse(message)` Parses Slack message and returns a tree ([`Node`](#interface-node)).
-
-
docs/examples.md (new)
-
@@ -0,0 +1,15 @@# Examples ## HTML + TypeScript This example shows the most basic usage of the library. The code handles only a limited set of node type for demonstration purpose. <iframe src="https://codesandbox.io/embed/gracious-rgb-kmqbu?fontsize=14&module=%2Fsrc%2Findex.ts&view=editor" loading="lazy" title="Plain HTML with TypeScript example" allow="" sandbox="allow-popups allow-scripts allow-same-origin"></iframe> ## React + TypeScript This example shows how to construct React node without using `dangerouslySetInnerHTML`. The code handles only a limited set of node type for demonstration purpose. <iframe src="https://codesandbox.io/embed/condescending-heyrovsky-y5jw3?fontsize=14&module=%2Fsrc%2FSlackMessage.tsx" loading="lazy" title="React with TypeScript example" allow="" sandbox="allow-popups allow-scripts allow-same-origin"></iframe>
-
-
docs/installation.md (new)
-
@@ -0,0 +1,39 @@# Installation This library is available for Node.js, Deno, and browser (or other ESM environment). ## Node.js Install `slack-message-parser` package hosted on NPM. ```sh # Use your package manager's install command npm i slack-message-parser ``` ```ts import { parse } from "slack-message-parser"; console.dir(parse("Slack *message* ~to~ _parse_")); ``` ## Deno This library is not registered to `deno.land` yet. Please use `raw.githubusercontent.com` for a moment. ```js import { parse } from "https://raw.githubusercontent.com/pocka/slack-message-parser/master/mod.ts"; console.dir(parse("Slack *message* ~to~ _parse_")); ``` ## Browser Use NPM-to-ESM service such as [Skypack](https://www.skypack.dev/). ```js import { parse } from "https://cdn.skypack.dev/slack-message-parser@^2.0.2"; console.dir(parse("Slack *message* ~to~ _parse_")); ```
-
-
docs/introduction.md (new)
-
@@ -0,0 +1,101 @@# Introduction slack-message-parser is a JavaScript library to parse a message returned by Slack API then give you an AST-like tree object. You can use the result tree to create a message viewer (HTML, React component, etc...) or whatever you want. ## Important note There are some cases it's impossible to correctly parse a message composed by [Slack's WYSIWYG message editor][slack-wyswig], especially when an inline code block is involved (see [#26]). Please consider traversing Blocks (`blocks` property) first. This library is suitable for fallback purpose or for when Blocks are not available. [slack-wyswig]: https://api.slack.com/changelog/2019-09-what-they-see-is-what-you-get-and-more-and-less [#26]: https://github.com/pocka/slack-message-parser/issues/26 ## Quick glance ```ts import slackMessageParser from "slack-message-parser"; const tree = slackMessageParser("Slack *message* ~to~ _parse_"); console.dir(tree); ``` <details> <summary>Output</summary> ```js ({ type: NodeType.Root, children: [ { type: NodeType.Text, text: "Slack ", source: "Slack ", }, { type: NodeType.Bold, children: [ { type: NodeType.Text, text: "message", source: "message", }, ], source: "*message*", }, { type: NodeType.Text, text: " ", source: " ", }, { type: NodeType.Strike, children: [ { type: NodeType.Text, text: "to", source: "to", }, ], source: "~to~", }, { type: NodeType.Text, text: " ", source: " ", }, { type: NodeType.Italic, children: [ { type: NodeType.Text, text: "parse", source: "parse", }, ], source: "_parse_", }, ], source: "Slack *message* ~to~ _parse_", }); ``` </details> ## Available Syntax - Plain text - Links - Channels (`#channel`) - User (`@someone`) - URL (`https://foo.bar`, `mailto:foo@bar`) - Commands (Represented as `<!foo>`, more details [here](https://api.slack.com/docs/message-formatting)) - Emojis 😍 - Code block - Inline code (<code>\`foo\`</code>) - Italic (`_foo_`) - Bold (`*foo*`) - Strikethrough (`~foo~`) - Quotes (`> foo`)
-
-
docs/site/build.tsx (new)
-
@@ -0,0 +1,219 @@/** @jsx h */ import initLightningCss, { transform as transformCss, } from "./deps/lightning_css.ts"; import { Fragment, h, Helmet, renderSSR, withStyles } from "./deps/nano_jsx.ts"; import { Layout } from "./components/Layout.tsx"; import { Nav } from "./components/Nav.tsx"; import * as csp from "./csp.ts"; import { Api, ApiToc } from "./pages/Api.tsx"; import { Examples, ExamplesToc } from "./pages/Examples.tsx"; import { Installation, InstallationToc } from "./pages/Installation.tsx"; import { Introduction, IntroductionToc } from "./pages/Introduction.tsx"; const baseURL = new URL(Deno.args[0] || "http://localhost:4507"); const pages = { intro: "", install: "installation/", examples: "examples/", api: "api/", } as const; const globalStyles = await Deno.readTextFile( new URL("global.css", import.meta.url), ); interface AppProps { baseURL: URL; page: keyof typeof pages; } const App = ({ baseURL, page }: AppProps) => { const { content, toc } = (() => { switch (page) { case "intro": return { toc: <IntroductionToc />, content: <Introduction />, }; case "install": return { toc: <InstallationToc />, content: <Installation />, }; case "examples": return { toc: <ExamplesToc />, content: <Examples />, }; case "api": return { toc: <ApiToc />, content: <Api />, }; } })(); return ( <Fragment> <Helmet> <link rel="stylesheet" href="https://rsms.me/inter/inter.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hack-font@3.3.0/build/web/hack-subset.css" /> </Helmet> <Layout siteRoot={baseURL} navigation={<Nav siteRoot={baseURL} />} toc={toc} > {content} </Layout> </Fragment> ); }; const tagPattern = /^<([^>]+)>([\s\S]*)<\/[^>]+>$/m; function separateInnerHTML(outerHTML: string): { tagName: string; innerHTML: string; outerHTML: string; } | { outerHTML: string; } { const match = outerHTML.match(tagPattern); if (!match) { return { outerHTML, }; } return { tagName: match[1], innerHTML: match[2], outerHTML, }; } const policies: csp.Policies = { "default-src": [csp.SELF], "frame-src": ["https://codesandbox.io"], "style-src": [ new URL("styles.css", baseURL).toString(), "https://rsms.me/inter/", "https://cdn.jsdelivr.net/npm/hack-font@3.3.0/", ], "font-src": [ "https://rsms.me/inter/", "https://cdn.jsdelivr.net/npm/hack-font@3.3.0/", ], }; interface GenerateParams { baseURL: URL; page: keyof typeof pages; buildDir: URL; styles: Map<string, boolean>; } async function generatePage( { baseURL, page, buildDir, styles }: GenerateParams, ) { const app = renderSSR( withStyles(globalStyles)(<App baseURL={baseURL} page={page} />), ); const { body, head, footer } = Helmet.SSR(app); const [headTags, styleTags] = head.map(separateInnerHTML).reduce< [ ReturnType<typeof separateInnerHTML>[], Extract<ReturnType<typeof separateInnerHTML>, { tagName: string }>[], ] >( ([rest, styles], tag) => "tagName" in tag && tag.tagName === "style" ? [rest, [...styles, tag]] : [[...rest, tag], styles], [[], []], ); styleTags.forEach(({ innerHTML }) => { styles.set(innerHTML, true); }); const cssPath = new URL("styles.css", baseURL).pathname; const html = ` <!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8" /> <meta http-equiv="Content-Security-Policy" content="${ csp.build(policies) }" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> ${headTags.map(({ outerHTML }) => outerHTML).join("\n")} <link rel="stylesheet" href="${cssPath}" /> </head> <!-- This site is generated using nano-jsx SSR, lightning-css, remark/rehype, Prism, and Deno. Source code at https://github.com/pocka/slack-message-parser/tree/master/docs/site/ --> <body> ${body} ${footer.join("\n")} </body> </html> `; const targetPath = new URL(`${pages[page]}index.html`, buildDir); const parentDir = new URL("./", targetPath); await Deno.mkdir(parentDir, { recursive: true }); await Deno.writeTextFile(targetPath, html); } const buildDir = new URL("./build/", import.meta.url); const styles = new Map<string, boolean>(); await Promise.all( (Object.keys(pages) as (keyof typeof pages)[]).map((page) => generatePage({ baseURL, page, buildDir, styles, }) ), ); await initLightningCss(); const css = Array.from(styles.keys()).join("\n"); const { code: transformedCss } = transformCss({ filename: "styles.css", code: new TextEncoder().encode(css), minify: true, targets: { chrome: (105 << 16), safari: (14 << 16), firefox: (102 << 16), }, }); await Deno.writeFile( new URL("styles.css", buildDir), transformedCss, );
-
-
-
@@ -0,0 +1,165 @@/** @jsx h */ import { Fragment, h, withStyles } from "../deps/nano_jsx.ts"; function cx(...classNames: string[]): string { return classNames.map((className) => `layout__${className}`).join(" "); } interface LayoutProps { navigation: unknown; toc: unknown; children: unknown; siteRoot?: URL; } export const Layout = ( { children, navigation, toc, siteRoot = new URL("https://example.com/") }: LayoutProps, ) => { return withStyles(css)( <Fragment> <div class={cx("logo-wrapper", "header")}> <a class={cx("logo")} href={new URL("./", siteRoot).pathname}> slack<br />message<br />parser </a> </div> <nav class={cx("nav", "header")}>{navigation}</nav> <div class={cx("header-fill", "header")} /> <aside class={cx("toc")}>{toc}</aside> <main class={cx("main")}> {children} </main> </Fragment>, ); }; const css = ` html { --_scroll-padding-top: 7rem; scroll-padding-top: var(--_scroll-padding-top); scroll-behavior: smooth; } body { display: grid; grid-template-columns: min-content minmax(0, 1fr); grid-template-rows: min-content max-content minmax(0, 1fr); grid-template-areas: "logo nav" "toc toc" "main main"; width: 100%; background-color: hsl(var(--color-neutral)); color: hsl(var(--color-on-neutral)); overflow-y: auto; } .${cx("header")} { position: sticky; top: 0; border-bottom: var(--border-width) solid hsl(var(--color-border)); background-color: hsl(var(--color-panel)); z-index: 666; } .${cx("logo-wrapper")} { grid-area: logo; border-right: var(--border-width) solid hsl(var(--color-border)); display: flex; justify-content: flex-end; align-items: center; padding: var(--gutter-md) var(--gutter-lg); } .${cx("logo")} { display: block; font-size: var(--font-size-sm); line-height: 1; color: inherit; font-weight: bold; text-align: right; text-decoration: none; } .${cx("logo")}:hover, .${cx("logo")}:focus-visible { outline: none; text-decoration: underline; } .${cx("nav")} { grid-area: nav; display: flex; justify-content: flex-start; align-items: stretch; flex-wrap: nowrap; padding: var(--gutter-sm) var(--gutter-lg); overflow-x: auto; } .${cx("header-fill")} { grid-area: fill; display: none; } .${cx("toc")} { grid-area: toc; display: block; border-bottom: var(--border-width) solid hsl(var(--color-border)); padding: var(--gutter-md); } .${cx("main")} { grid-area: main; display: block; padding: var(--gutter-md); } @media (min-width: 800px) { body { grid-template-columns: 20rem minmax(0, 1fr); grid-template-rows: min-content minmax(0, 1fr); grid-template-areas: "logo nav" "toc main"; } .${cx("toc")} { align-self: start; position: sticky; top: var(--_scroll-padding-top); display: flex; justify-content: flex-end; border: 0; } .${cx("main")} { flex: 1; padding: var(--gutter-lg); } .${cx("nav")} { justify-content: center; } } @media (min-width: 1100px) { body { grid-template-columns: minmax(0, 1fr) 70rem minmax(0, 1fr); grid-template-areas: "logo nav fill" "toc main unused"; } .${cx("header-fill")} { display: block; } } `;
-
-
-
@@ -0,0 +1,162 @@/** @jsx h */ import { h, withStyles } from "../deps/nano_jsx.ts"; function cx(className: string): string { return `markdown__${className}`; } interface MarkdownProps { /** * Parsed HTML string. */ html: string; } /** * Component to render parsed markdown content. */ export const Markdown = ({ html }: MarkdownProps) => { return withStyles(css)( <div class={cx("root")} dangerouslySetInnerHTML={{ __html: html }} />, ); }; const css = ` .${cx("root")} h1 { font-size: var(--font-size-xl); font-weight: bold; } .${cx("root")} h2 { font-size: var(--font-size-lg); font-weight: bold; margin-top: 2em; } .${cx("root")} h3 { font-size: var(--font-size-md); font-weight: bold; margin-top: 1.5em; } .${cx("root")} h4 { font-size: var(--font-size-md); font-weight: bold; margin-top: 1em; } @keyframes ${cx("blink")} { from { color: hsl(var(--color-on-neutral)); } to { color: hsl(var(--color-primary)); } } .${cx("root")} h1:target, .${cx("root")} h2:target, .${cx("root")} h3:target { animation: 0.3s 0s linear alternate 4 ${cx("blink")}; } .${cx("root")} p { line-height: var(--line-height-normal); margin-top: 0.5em; } .${cx("root")} a { color: hsl(var(--color-primary)); text-decoration: none; } .${cx("root")} a:hover, .${cx("root")} a:focus-visible { text-decoration: underline; } .${cx("root")} code:not(pre > code), .${cx("root")} pre { padding: var(--gutter-xs) var(--gutter-md); font-family: var(--font-family-mono); background-color: hsl(var(--color-panel)); border-radius: var(--radius); color: hsl(var(--color-on-panel)); } .${cx("root")} pre { margin-top: 1em; width: 100%; padding: var(--gutter-md) var(--gutter-lg); border: var(--border-width) solid hsla(var(--color-border)); line-height: var(--line-height-dense); overflow-x: auto; } .${cx("root")} details { margin-top: 1em; } .${cx("root")} [class^="language-"] .token.keyword { color: hsl(var(--color-code-keyword)); } .${cx("root")} [class^="language-"] .token.string { color: hsl(var(--color-code-string)); } .${cx("root")} [class^="language-"] .token.operator { color: hsl(var(--color-code-operator)); } .${cx("root")} [class^="language-"] .token.function { color: hsl(var(--color-code-function)); } .${cx("root")} [class^="language-"] .token.comment { color: hsl(var(--color-code-comment)); font-style: italic; } .${cx("root")} ul { margin-top: 0.5em; padding-left: 1em; } .${cx("root")} ul ul { margin-top: 0; } .${cx("root")} iframe { margin-top: 1em; width: 100%; aspect-ratio: 4 / 3; border: var(--border-width) solid hsla(var(--color-border)); border-radius: var(--radius); } .${cx("root")} table { width: 100%; margin-top: 1em; /* border-collapse: collapse; */ border-spacing: 0; border: var(--border-width) solid hsla(var(--color-border)); border-radius: var(--radius); } .${cx("root")} thead { background-color: hsl(var(--color-panel)); color: hsl(var(--color-on-panel)); } .${cx("root")} th, .${cx("root")} tr:not(:last-child) > td { border-bottom: var(--border-width) solid hsla(var(--color-border)); } .${cx("root")} th, .${cx("root")} td { padding: var(--gutter-sm) var(--gutter-md); } `;
-
-
-
@@ -0,0 +1,69 @@/** @jsx h */ import { h, withStyles } from "../deps/nano_jsx.ts"; function cx(className: string): string { return `nav__${className}`; } interface NavProps { siteRoot?: URL; } export const Nav = ( { siteRoot = new URL("https://example.com/") }: NavProps, ) => { const path = (relPath: string) => new URL(relPath, siteRoot).pathname; return withStyles(css)( <ul class={cx("list")}> <li> <a class={cx("link")} href={path("./")}>Intro</a> </li> <li> <a class={cx("link")} href={path("installation/")}>Install</a> </li> <li> <a class={cx("link")} href={path("api/")}>API</a> </li> <li> <a class={cx("link")} href={path("examples/")}>Examples</a> </li> <li> <a class={cx("link")} href="https://github.com/pocka/slack-message-parser" > GitHub </a> </li> </ul>, ); }; const css = ` .${cx("list")} { display: flex; justify-content: flex-start; align-items: center; font-size: var(--font-size-sm); list-style: none; } .${cx("link")} { padding: var(--gutter-md) var(--gutter-lg); border: var(--border-width) solid transparent; border-radius: 3px; color: inherit; text-decoration: none; } .${cx("link")}:hover { background-color: hsla(var(--color-primary), 0.3); border-color: hsl(var(--color-primary)); } .${cx("link")}:focus-visible { outline: none; text-decoration: underline; } `;
-
-
-
@@ -0,0 +1,6 @@/** @jsx h */ import { h } from "../deps/nano_jsx.ts"; export const Title = ({ children }: { children: unknown }) => ( <title>{children} | slack-message-parser</title> );
-
-
-
@@ -0,0 +1,67 @@/** @jsx h */ import { h, withStyles } from "../deps/nano_jsx.ts"; function cx(className: string): string { return `toc__${className}`; } type TocProps = { /** * Inner HTML. * * Must be `<nav><ol>...</ol></nav>` */ children: unknown; } | { /** * Inner HTML. * * Must be `<nav><ol>...</ol></nav>` */ html: string; }; /** * Table of Contents component. */ export const Toc = (props: TocProps) => { return withStyles(css)( "html" in props ? ( <div class={cx("root")} dangerouslySetInnerHTML={{ __html: props.html }} /> ) : <div class={cx("root")}>{props.children}</div>, ); }; const css = ` .${cx("root")} { min-width: 15rem; } .${cx("root")} ol { list-style: none; } .${cx("root")} ol ol { padding-left: var(--gutter-lg); list-style: square; } .${cx("root")} ol ol ol { list-style: circle; } .${cx("root")} a { font-size: var(--font-size-sm); color: hsl(var(--color-on-neutral)); text-decoration: underline hsl(var(--color-border)); opacity: 0.9; } .${cx("root")} a:hover { text-decoration: underline calc(var(--border-width) * 2) hsl(var(--color-primary)); } `;
-
-
docs/site/csp.ts (new)
-
@@ -0,0 +1,24 @@export const SELF = `'self'`; export async function sha256(content: string): Promise<string> { const enc = new TextEncoder(); const buf = await crypto.subtle.digest("SHA-256", enc.encode(content)); const arr = Array.from(new Uint8Array(buf)); const base64 = btoa(arr.map((b) => String.fromCharCode(b)).join("")); return `'sha256-${base64}'`; } export interface Policies { [name: string]: string[]; } /** * Build CSP Header value. */ export function build(policies: Policies): string { return Object.entries(policies).map(([name, values]) => `${name} ${values.join(" ")}` ).join(";"); }
-
-
-
@@ -0,0 +1,2 @@export * from "https://unpkg.com/lightningcss-wasm@1.16.0/index.js"; export { default } from "https://unpkg.com/lightningcss-wasm@1.16.0/index.js";
-
-
-
@@ -0,0 +1,1 @@export * from "https://deno.land/x/nano_jsx@v0.0.34/mod.ts";
-
-
docs/site/deps/rehype.ts (new)
-
@@ -0,0 +1,4 @@export { default as rehypeSlug } from "https://esm.sh/rehype-slug@5.1.0"; export { default as rehypeToc } from "https://esm.sh/@jsdevtools/rehype-toc@3.0.2"; export { default as rehypeStringify } from "https://esm.sh/rehype-stringify@9.0.3"; export { default as rehypePrism } from "https://esm.sh/@mapbox/rehype-prism@0.8.0";
-
-
docs/site/deps/remark.ts (new)
-
@@ -0,0 +1,3 @@export { default as remarkParse } from "https://esm.sh/remark-parse@10.0.1"; export { default as remarkGfm } from "https://esm.sh/remark-gfm@3.0.1"; export { default as remarkRehype } from "https://esm.sh/remark-rehype@10.1.0";
-
-
-
@@ -0,0 +1,1 @@export * from "https://esm.sh/unified@10.1.2";
-
-
-
@@ -0,0 +1,1 @@export * from "https://esm.sh/unist-util-visit@4.1.1";
-
-
docs/site/global.css (new)
-
@@ -0,0 +1,86 @@:root { --font-size-sm: 1.4rem; --font-size-md: 1.6rem; --font-size-lg: 1.8rem; --font-size-xl: 2.2rem; --line-height-dense: 1.5; --line-height-normal: 1.75; --line-height-sparse: 2; --gutter-xs: 2px; --gutter-sm: 4px; --gutter-md: 8px; --gutter-lg: 16px; --gutter-xl: 24px; --font-family: 'Inter', sans-serif; --font-family-mono: Hack, monospace; --radius: 3px; --border-width: 1px; --color-neutral: 0, 0%, 93%; --color-on-neutral: 0, 0%, 5%; --color-primary: 100, 35%, 35%; --color-on-primary: 0, 0%, 95%; --color-panel: 100, 5%, 90%; --color-on-panel: 0, 0%, 5%; --color-danger: 20, 50%, 40%; --color-on-danger: 0, 0%, 5%; --color-border: 0, 0%, 0%, 0.25; --color-code-comment: 20, 0%, 30%; --color-code-keyword: 20, 90%, 30%; --color-code-string: 60, 60%, 30%; --color-code-operator: var(--color-code-keyword); --color-code-function: 200, 80%, 30%; } @media (prefers-color-scheme: dark) { :root { --color-neutral: 0, 0%, 15%; --color-on-neutral: 0, 0%, 95%; --color-primary: 100, 30%, 45%; --color-on-primary: 0, 0%, 95%; --color-panel: 100, 5%, 10%; --color-on-panel: 0, 0%, 95%; --color-danger: 20, 45%, 50%; --color-on-danger: 0, 0%, 95%; --color-border: 0, 0%, 95%, 0.2; --color-code-comment: 20, 0%, 60%; --color-code-keyword: 20, 80%, 65%; --color-code-string: 60, 40%, 55%; --color-code-operator: var(--color-code-keyword); --color-code-function: 200, 60%, 50%; } } *, *::before, *::after { box-sizing: border-box; padding: 0; margin: 0; font: inherit; } html { font-size: 62.5%; } body { font-size: var(--font-size-md); line-height: var(--line-height-normal); margin: 0; padding: 0; } html { font-family: var(--font-family); } @supports (font-variation-settings: normal) { :root { --font-family: 'Inter var', sans-serif; } }
-
-
docs/site/markdown.ts (new)
-
@@ -0,0 +1,91 @@// deno-lint-ignore-file no-explicit-any import { Plugin, Transformer, unified } from "./deps/unified.ts"; import { remarkGfm, remarkParse, remarkRehype } from "./deps/remark.ts"; import { rehypePrism, rehypeSlug, rehypeStringify, rehypeToc, } from "./deps/rehype.ts"; import { SKIP, visit } from "./deps/unist_util_visit.ts"; const rehypeNavOnly: Plugin = () => { const transformer: Transformer = (tree) => { visit(tree, "element", (node: any, index: number, parent: any) => { if (parent?.type === "root" && node.tagName !== "nav") { parent.children.splice(index, 1); return [SKIP, index]; } }); visit( tree, (node: any) => ["text", "raw"].includes(node.type), (_node: any, index: number | null, parent: any) => { if (parent?.type === "root" && typeof index === "number") { parent.children.splice(index, 1); return [SKIP, index]; } }, ); }; return transformer; }; interface ParseResult { /** * Parsed HTML */ main: string; /** * Table of contents HTML */ toc: string; } interface ParseOptions { path: URL | string; } const baseProcessor = () => unified().use(remarkParse).use(remarkGfm).use(remarkRehype, { allowDangerousHtml: true, }).use(rehypeSlug); const mainProcessor = baseProcessor().use(rehypePrism, { alias: { "shell": "sh", }, }).use(rehypeStringify, { allowDangerousHtml: true, }); const tocProcessor = baseProcessor().use(rehypeToc, { headings: ["h1", "h2", "h3"], }).use(rehypeNavOnly).use( rehypeStringify, ); export async function parseMarkdownDocs(docsMd: string): Promise<string> { return (await mainProcessor.process(docsMd)).toString(); } export async function parseMarkdownFile( { path }: ParseOptions, ): Promise<ParseResult> { const content = new TextDecoder("utf-8").decode(await Deno.readFile(path)); const [main, toc] = await Promise.all([ mainProcessor.process(content), tocProcessor.process(content), ]); return { main: main.toString(), toc: toc.toString(), }; }
-
-
docs/site/pages/Api.tsx (new)
-
@@ -0,0 +1,22 @@/** @jsx h */ import { Fragment, h, Helmet } from "../deps/nano_jsx.ts"; import { Markdown } from "../components/Markdown.tsx"; import { Title } from "../components/Title.tsx"; import { Toc } from "../components/Toc.tsx"; import { parseMarkdownFile } from "../markdown.ts"; const { toc, main } = await parseMarkdownFile({ path: new URL("../../api.md", import.meta.url), }); export const ApiToc = () => <Toc html={toc} />; export const Api = () => ( <Fragment> <Helmet> <Title>API</Title> </Helmet> <Markdown html={main} /> </Fragment> );
-
-
-
@@ -0,0 +1,49 @@/** @jsx h */ import { Fragment, h, Helmet, withStyles } from "../deps/nano_jsx.ts"; import { Markdown } from "../components/Markdown.tsx"; import { Title } from "../components/Title.tsx"; import { Toc } from "../components/Toc.tsx"; import { parseMarkdownFile } from "../markdown.ts"; const { toc, main } = await parseMarkdownFile({ path: new URL("../../examples.md", import.meta.url), }); function cx(className: string): string { return `pages_examples__${className}`; } export const ExamplesToc = () => { return <Toc html={toc} />; }; export const Examples = () => { return withStyles(css)( <Fragment> <Helmet> <Title>Examples</Title> </Helmet> <div> <noscript> <div class={cx("alert")}> This page does not work with JavaScript disabled. </div> </noscript> <Markdown html={main} /> </div> </Fragment>, ); }; const css = ` .${cx("alert")} { padding: var(--gutter-md); border: var(--border-width) solid hsl(var(--color-danger)); margin-bottom: 1em; background-color: hsla(var(--color-danger), 0.3); border-radius: var(--radius); color: hsl(var(--color-on-neutral)); } `;
-
-
-
@@ -0,0 +1,22 @@/** @jsx h */ import { Fragment, h, Helmet } from "../deps/nano_jsx.ts"; import { Markdown } from "../components/Markdown.tsx"; import { Title } from "../components/Title.tsx"; import { Toc } from "../components/Toc.tsx"; import { parseMarkdownFile } from "../markdown.ts"; const { toc, main } = await parseMarkdownFile({ path: new URL("../../installation.md", import.meta.url), }); export const InstallationToc = () => <Toc html={toc} />; export const Installation = () => ( <Fragment> <Helmet> <Title>Installation</Title> </Helmet> <Markdown html={main} /> </Fragment> );
-
-
-
@@ -0,0 +1,22 @@/** @jsx h */ import { Fragment, h, Helmet } from "../deps/nano_jsx.ts"; import { Markdown } from "../components/Markdown.tsx"; import { Title } from "../components/Title.tsx"; import { Toc } from "../components/Toc.tsx"; import { parseMarkdownFile } from "../markdown.ts"; const { toc, main } = await parseMarkdownFile({ path: new URL("../../introduction.md", import.meta.url), }); export const IntroductionToc = () => <Toc html={toc} />; export const Introduction = () => ( <Fragment> <Helmet> <Title>Introduction</Title> </Helmet> <Markdown html={main} /> </Fragment> );
-
-
-
@@ -61,7 +61,7 @@ | Strike| Quote | Root; interface NodeBase { export interface NodeBase { type: NodeType; /**
-