('123');
102 | // ~~~~~
103 | // Argument of type 'string' is not assignable to parameter of type 'number'.
104 | ```
105 |
--------------------------------------------------------------------------------
/.github/assets/banner.svg:
--------------------------------------------------------------------------------
1 |
92 |
--------------------------------------------------------------------------------
/docs/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { AppProps as NextAppProps } from 'next/app';
3 | import Head from 'next/head';
4 | import { NextRouter } from 'next/router';
5 | import { MarkdocNextJsPageProps } from '@markdoc/next.js';
6 |
7 | import { ErrorLayout, DocsLayout, HomeLayout } from '../components/layouts';
8 | import { Footer, Header, SideNavContext, useSidenavState } from '../components/shell';
9 |
10 | import '../public/global.css';
11 |
12 | // Types
13 | // ------------------------------
14 |
15 | type AppProps = {
16 | pageProps: P;
17 | router: NextRouter;
18 | } & Omit, 'pageProps'>;
19 |
20 | type PageProps = MarkdocNextJsPageProps & {
21 | isErrorPage?: boolean;
22 | };
23 |
24 | // App
25 | // ------------------------------
26 |
27 | const BRAND = 'Emery';
28 | const SUMMARY = 'Polish for the rough parts of TypeScript.';
29 |
30 | export default function MyApp(props: AppProps) {
31 | const sidenavContext = useSidenavState();
32 | const { Component, pageProps, router } = props;
33 | const { markdoc, isErrorPage } = pageProps;
34 |
35 | let title = BRAND;
36 | let description = SUMMARY;
37 | if (markdoc) {
38 | if (markdoc.frontmatter.title) {
39 | title = markdoc.frontmatter.title;
40 | }
41 | if (markdoc.frontmatter.description) {
42 | description = markdoc.frontmatter.description;
43 | }
44 | }
45 | const isHome = router.pathname === '/';
46 | const isDocs = !isHome && !isErrorPage;
47 |
48 | const Layout = (() => {
49 | if (isErrorPage) {
50 | return ErrorLayout;
51 | }
52 | if (isHome) {
53 | return HomeLayout;
54 | }
55 | if (isDocs) {
56 | return DocsLayout;
57 | }
58 |
59 | return Fragment;
60 | })();
61 |
62 | const brandAppendedTitle = `${title} — ${BRAND}`;
63 |
64 | return (
65 | <>
66 |
67 | {isHome ? `${BRAND} - ${title}` : brandAppendedTitle}
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | {isDocs ? (
85 | <>
86 |
87 |
88 | >
89 | ) : (
90 | <>
91 |
92 |
93 | >
94 | )}
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | {isHome ? : null}
111 |
112 | >
113 | );
114 | }
115 |
116 | // Utils
117 | // ------------------------------
118 |
119 | function canonicalUrl(path?: string) {
120 | const url = 'https://emery-ts.vercel.app';
121 | if (!path) return url;
122 | return url + path;
123 | }
124 |
--------------------------------------------------------------------------------
/docs/pages/docs/checks.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Checks
3 | description: Utilities for dealing with ambiguous types
4 | ---
5 |
6 | # {% $markdoc.frontmatter.title %}
7 |
8 | Emery considers "checks" predicates that cannot be expressed as [type guards](/docs/guards), without enforcing [opaque types](/docs/opaques). While we recommend opaque types where appropriate, we can't make assumptions about your program's requirements.
9 |
10 | ## Utils
11 |
12 | While the available checks are useful for specific cases, we expose a handful of convenient utility functions to extend their behaviour or create your own purpose-built checks.
13 |
14 | ### checkAll
15 |
16 | Returns a new function for checking _all_ cases against a value, a bit like `pipe` for predicates.
17 |
18 | ```ts
19 | function checkAll(...predicates: UnaryPredicate[]): UnaryPredicate;
20 | ```
21 |
22 | Useful for creating a new predicate that combines all those provided, which can be called elsewhere in your program.
23 |
24 | ```ts
25 | import { checkAll, isNonNegative, isInteger } from 'emery';
26 |
27 | export const isNonNegativeInteger = checkAll(isNonNegative, isInteger);
28 | ```
29 |
30 | When combined with [assertions](/docs/assertions), checks become incredibly powerful for simplifying logic.
31 |
32 | ```ts
33 | import { assert } from 'emery';
34 | import { isNonNegativeInteger } from './path-to/check';
35 |
36 | function getThingByIndex(index: number) {
37 | assert(isNonNegativeInteger(index));
38 |
39 | // safely use `index`
40 | return things[index];
41 | }
42 | ```
43 |
44 | ### checkAllWith
45 |
46 | Apply _all_ checks against a value.
47 |
48 | ```ts
49 | function checkAllWith(value: T, ...predicates: UnaryPredicate[]): boolean;
50 | ```
51 |
52 | Useful for calling predicates immediately:
53 |
54 | ```ts
55 | function getThingByIndex(index: number) {
56 | assert(checkAllWith(index, isNonNegative, isInteger));
57 |
58 | // safely use `index`
59 | return things[index];
60 | }
61 | ```
62 |
63 | ### negate
64 |
65 | Returns a new negated version of the stated predicate function.
66 |
67 | ```ts
68 | function negate(predicate: UnaryPredicate): UnaryPredicate;
69 | ```
70 |
71 | Useful for inverting an existing predicate:
72 |
73 | ```ts
74 | const hasSpaces = (value: string) => /\s/g.test(value);
75 | const hasNoSpaces = negate(hasSpaces);
76 | ```
77 |
78 | ## Number
79 |
80 | Common checks for `number` types.
81 |
82 | ### isFinite
83 |
84 | Checks whether a number is finite.
85 |
86 | ```ts
87 | isFinite(1); // → true
88 | isFinite(Number.POSITIVE_INFINITY); // → false
89 | isFinite(Number.NEGATIVE_INFINITY); // → false
90 | ```
91 |
92 | ### isInfinite
93 |
94 | Checks whether a number is infinite.
95 |
96 | ```ts
97 | isInfinite(Number.POSITIVE_INFINITY); // → true
98 | isInfinite(Number.NEGATIVE_INFINITY); // → true
99 | isInfinite(1); // → false
100 | ```
101 |
102 | ### isInteger
103 |
104 | Checks whether a number is an integer.
105 |
106 | ```ts
107 | isInteger(1); // → true
108 | isInteger(Number.MAX_SAFE_INTEGER); // → true
109 | isInteger(1.2); // → false
110 | ```
111 |
112 | ### isFloat
113 |
114 | Checks whether a number is a float.
115 |
116 | ```ts
117 | isFloat(1.2); // → true
118 | isFloat(-1.2); // → true
119 | isFloat(1); // → false
120 | ```
121 |
122 | ### isEven
123 |
124 | Checks whether a number is even.
125 |
126 | ```ts
127 | isEven(2); // → true
128 | isEven(-2); // → true
129 | isEven(1); // → false
130 | ```
131 |
132 | ### isOdd
133 |
134 | Checks whether a number is odd.
135 |
136 | ```ts
137 | isOdd(1); // → true
138 | isOdd(-1); // → true
139 | isOdd(2); // → false
140 | ```
141 |
142 | ### isNegativeZero
143 |
144 | Checks whether a number is negative zero.
145 |
146 | ```ts
147 | isNegativeZero(-0); // → true
148 | isNegativeZero(0); // → false
149 | isNegativeZero(1); // → false
150 | ```
151 |
152 | ### isNegative
153 |
154 | Checks whether a number is negative.
155 |
156 | ```ts
157 | isNegative(-1); // → true
158 | isNegative(0); // → false
159 | isNegative(1); // → false
160 | ```
161 |
162 | ### isPositive
163 |
164 | Checks whether a number is positive.
165 |
166 | ```ts
167 | isPositive(1); // → true
168 | isPositive(0); // → false
169 | isPositive(-1); // → false
170 | ```
171 |
172 | ### isNonNegative
173 |
174 | Checks whether a number is **not** negative.
175 |
176 | ```ts
177 | isNonNegative(1); // → true
178 | isNonNegative(0); // → true
179 | isNonNegative(-1); // → false
180 | ```
181 |
182 | ### isNonPositive
183 |
184 | Checks whether a number is **not** positive.
185 |
186 | ```ts
187 | isNonPositive(-1); // → true
188 | isNonPositive(0); // → true
189 | isNonPositive(1); // → false
190 | ```
191 |
--------------------------------------------------------------------------------
/docs/components/shell/SideNav.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, Fragment, useContext, useState } from 'react';
2 | import { useRouter } from 'next/router';
3 | import Link from 'next/link';
4 |
5 | export const navID = 'docs-navigation';
6 |
7 | const items = [
8 | {
9 | title: 'About',
10 | links: [
11 | { href: '/docs/getting-started', children: 'Getting started' },
12 | { href: '/docs/origin-story', children: 'Origin story' },
13 | ],
14 | },
15 | {
16 | title: 'Concepts',
17 | links: [
18 | { href: '/docs/assertions', children: 'Assertions' },
19 | { href: '/docs/checks', children: 'Checks' },
20 | { href: '/docs/guards', children: 'Guards' },
21 | { href: '/docs/opaques', children: 'Opaques' },
22 | { href: '/docs/utils', children: 'Utils' },
23 | ],
24 | },
25 | ];
26 |
27 | export function SideNav() {
28 | const router = useRouter();
29 | const [sidenavOpen, setSidenavOpen] = useSidenav();
30 |
31 | return (
32 |
148 | );
149 | }
150 |
151 | // Context
152 | // ------------------------------
153 |
154 | type SideNavContextType = ReturnType;
155 | export const SideNavContext = createContext(null);
156 | export const useSidenav = () => {
157 | const ctx = useContext(SideNavContext);
158 |
159 | if (!ctx) {
160 | throw Error('Attempt to use `SideNavContext` outside of its provider.');
161 | }
162 |
163 | return ctx;
164 | };
165 | export const useSidenavState = () => useState(false);
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Emery
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ---
22 |
23 | 💎 Polish for the rough parts of TypeScript.
24 |
25 | TypeScript is great but there's parts that are still rough around the edges, especially for developers who are new to the language.
26 |
27 | ## Purpose and intent
28 |
29 | Emery is a small collection of utilities that improve DX without compromising static types.
30 |
31 | ### Check for ambiguous types
32 |
33 | Emery exposes "checks" for dealing with ambiguous types.
34 |
35 | Checks are just predicates that can't be expressed as type guards, without enforcing opaque types.
36 |
37 | ```ts
38 | import { checkAll, isNonNegative, isInteger } from 'emery';
39 |
40 | /**
41 | * Along with some default check functions, we provide helpers
42 | * for managing combinations. The `checkAll` helper is a bit
43 | * like `pipe` for predicates.
44 | */
45 | export const isNonNegativeInteger = checkAll(isNonNegative, isInteger);
46 | ```
47 |
48 | ### Assert the validity of props
49 |
50 | An assertion declares that a condition be true before executing subsequent code, ensuring that whatever condition is checked must be true for the remainder of the containing scope.
51 |
52 | ```ts
53 | import { assert } from 'emery';
54 |
55 | import { isNonNegativeInteger } from './path-to/check';
56 |
57 | function getThingByIndex(index: number) {
58 | assert(isNonNegativeInteger(index));
59 |
60 | return things[index]; // 🎉 Safely use the `index` argument!
61 | }
62 | ```
63 |
64 | ### Smooth over loose types
65 |
66 | Utility functions for smoothing over areas of TypeScript that are loosely typed.
67 |
68 | Because of JavaScript's dynamic implementation the default TS behaviour is correct, but can be frustrating in certain situations.
69 |
70 | ```ts
71 | import { typedKeys } from 'emery';
72 |
73 | const obj = { foo: 1, bar: 2 };
74 |
75 | const thing = Object.keys(obj).map(key => {
76 | return obj[key]; // 🚨 'string' can't be used to index...
77 | });
78 | const thing2 = typedKeys(obj).map(key => {
79 | return obj[key]; // 🎉 No more TypeScript issues!
80 | });
81 | ```
82 |
83 | ## Philosophy and motivation
84 |
85 | Like all good things, Emery started with curiosity. At [Thinkmill](https://thinkmill.com.au/) we have an internal Slack channel for TypeScript where a question was raised about how to offer consumers error messages that convey intent, not just cascading type failures.
86 |
87 | While that's not currently possible, it became apparent that there was demand for a solution. We also discovered that many developers were carrying around miscellaneous utilities for working with TypeScript between projects.
88 |
89 | ---
90 |
91 | ## License
92 |
93 | Copyright (c) 2023
94 | [Thinkmill Labs](https://www.thinkmill.com.au/labs?utm_campaign=github-emery)
95 | Pty Ltd. Licensed under the MIT License.
96 |
--------------------------------------------------------------------------------
/docs/components/shell/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Hidden } from '../Hidden';
3 |
4 | type Mode = 'light' | 'dark';
5 |
6 | export function ThemeToggle() {
7 | const [theme, setTheme] = React.useState(null);
8 |
9 | function setPreferredTheme(newTheme: Mode) {
10 | setTheme(newTheme);
11 | try {
12 | localStorage.setItem('theme', newTheme);
13 | } catch (err) {
14 | //
15 | }
16 | }
17 |
18 | React.useEffect(() => {
19 | let preferredTheme: Mode | null = null;
20 | try {
21 | preferredTheme = localStorage.getItem('theme') as Mode;
22 | } catch (err) {
23 | //
24 | }
25 |
26 | const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
27 | darkQuery.addListener(e => setTheme(e.matches ? 'dark' : 'light'));
28 |
29 | setTheme(preferredTheme || (darkQuery.matches ? 'dark' : 'light'));
30 | }, []);
31 |
32 | React.useEffect(() => {
33 | if (theme) {
34 | document.body.className = theme;
35 | }
36 | }, [theme]);
37 |
38 | const isDark = theme === 'dark';
39 |
40 | return (
41 |
126 | );
127 | }
128 |
129 | const sun = (
130 |
137 |
138 |
139 |
140 |
147 |
154 |
161 |
168 |
175 |
182 |
183 | );
184 |
185 | const moon = (
186 |
193 |
198 |
199 | );
200 |
--------------------------------------------------------------------------------
/docs/components/shell/Header.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import React, { useMemo } from 'react';
3 | import { joinClasses } from '../../utils';
4 | import { Hidden } from '../Hidden';
5 |
6 | import { Link } from '../Link';
7 | import { useSidenav, navID } from './SideNav';
8 | import { ThemeToggle } from './ThemeToggle';
9 |
10 | export function Header() {
11 | const router = useRouter();
12 | const isHome = router.pathname === '/';
13 | const isDocs = router.pathname.startsWith('/docs');
14 | const [sidenavOpen, setSidenavOpen] = useSidenav();
15 |
16 | return (
17 |
161 | );
162 | }
163 |
164 | // Styled components
165 | // ------------------------------
166 |
167 | const Brand = () => {
168 | return (
169 |
170 | {logo}
171 | Emery
172 |
203 |
204 | );
205 | };
206 | const logo = (
207 |
208 |
212 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
229 |
230 | );
231 |
232 | const Hamburger = ({ active }: { active?: boolean }) => {
233 | const className = joinClasses(['hamburger', active && 'hamburger--active']);
234 |
235 | return (
236 |
247 | {useMemo(
248 | () =>
249 | ['top', 'middle', 'bottom'].map((pos, idx) => (
250 |
258 | )),
259 | [],
260 | )}
261 |
283 |
284 | );
285 | };
286 |
--------------------------------------------------------------------------------
/docs/public/global.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=family=Inter:wght@400;500;700;900&display=swap');
2 |
3 | :root {
4 | /* misc */
5 | --header-height: 54px;
6 | --page-max-width: 1280px;
7 | --sidenav-width: 240px;
8 | --scroll-offset: 3rem;
9 |
10 | /* border-radius */
11 | --radii-large: 6px;
12 | --radii-medium: 4px;
13 | --radii-small: 3px;
14 |
15 | /* spacing */
16 | --gutter-large: 3rem;
17 | --gutter: 2rem;
18 | --gutter-small: 1rem;
19 | --gutter-xsmall: 0.5rem;
20 |
21 | /* typographic spacing */
22 | --vertical-rhythm: 0.75rem;
23 | --vertical-rhythm-prominent: 1rem;
24 | /* --vertical-rhythm-dramatic: 2rem; */
25 |
26 | /* typography */
27 | --ff-sans: Inter, -apple-system, BlinkMacSystemFont, Inter, Segoe UI, Helvetica Neue, sans-serif;
28 | --ff-mono: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
29 |
30 | --fs-small: 0.85rem;
31 | --fs-standard: 1rem;
32 | --fs-large: 1.2rem;
33 |
34 | --hfs-1: 2.8rem;
35 | --hfs-2: 2rem;
36 | --hfs-3: 1.6rem;
37 | --hfs-4: 1.2rem;
38 | --hfs-5: 1rem;
39 | --hfs-6: 0.9rem;
40 |
41 | --fw-regular: 400;
42 | --fw-medium: 500;
43 | --fw-bold: 700;
44 | --fw-heavy: 900;
45 | }
46 |
47 | @media screen and (max-width: 600px) {
48 | :root {
49 | --header-height: 44px;
50 |
51 | --gutter: 1.4rem;
52 |
53 | --hfs-1: 2rem;
54 | --hfs-2: 1.6rem;
55 | --hfs-3: 1.4rem;
56 | --hfs-4: 1.2rem;
57 | --hfs-5: 1rem;
58 | --hfs-6: 0.9rem;
59 | }
60 | }
61 |
62 | :root,
63 | .light {
64 | --info: #0ea5e9;
65 | --warning: #f59e0b;
66 | --positive: #22c55e;
67 | --critical: #f43f5e;
68 |
69 | --purple: #7c3aed;
70 | --green: #0d9488;
71 | --blue: #2563eb;
72 | --magenta: #c026d3;
73 | --yellow: #ca8a04;
74 |
75 | --neutral-0: #ffffff;
76 | --neutral-50: #f8fafc;
77 | --neutral-100: #f1f5f9;
78 | --neutral-200: #e2e8f0;
79 | --neutral-300: #cbd5e1;
80 | --neutral-400: #94a3b8;
81 | --neutral-500: #64748b;
82 | --neutral-600: #475569;
83 | --neutral-700: #334155;
84 | --neutral-800: #1e293b;
85 | --neutral-900: #111827;
86 |
87 | --light: var(--neutral-0);
88 |
89 | --text-prominent: var(--neutral-900);
90 | --text: var(--neutral-800);
91 | --text-muted: var(--neutral-600);
92 | --text-dim: var(--neutral-400);
93 |
94 | --surface-prominent: var(--neutral-200);
95 | --surface: var(--neutral-100);
96 | --surface-muted: var(--neutral-50);
97 |
98 | --border-prominent: var(--neutral-300);
99 | --border: var(--neutral-200);
100 |
101 | --brand: var(--blue);
102 | }
103 |
104 | .dark {
105 | --info: #0284c7;
106 | --warning: #f59e0b;
107 | --positive: #15803d;
108 | --critical: #be123c;
109 |
110 | --purple: #a78bfa;
111 | --green: #2dd4bf;
112 | --blue: #60a5fa;
113 | --magenta: #e879f9;
114 | --yellow: #fef08a;
115 |
116 | --light: var(--neutral-900);
117 |
118 | --text-prominent: var(--neutral-0);
119 | --text: var(--neutral-100);
120 | --text-muted: var(--neutral-300);
121 | --text-dim: var(--neutral-500);
122 |
123 | --surface-prominent: var(--neutral-700);
124 | --surface: var(--neutral-800);
125 | --surface-muted: var(--neutral-900);
126 |
127 | --border-prominent: var(--neutral-600);
128 | --border: var(--neutral-700);
129 | --brand: var(--blue);
130 | }
131 |
132 | /*
133 | ============================================================
134 | * RESET
135 | ============================================================
136 | */
137 |
138 | html,
139 | body,
140 | #__next {
141 | height: 100%;
142 | margin: 0;
143 | padding: 0;
144 | }
145 |
146 | *,
147 | *::before,
148 | *::after {
149 | box-sizing: border-box;
150 | }
151 |
152 | * {
153 | margin: 0;
154 | }
155 |
156 | #__next {
157 | /* https://www.joshwcomeau.com/css/custom-css-reset/ */
158 | isolation: isolate;
159 | }
160 |
161 | body {
162 | color: var(--text);
163 | background: var(--light);
164 | font-family: var(--ff-sans);
165 |
166 | font-feature-settings: 'liga';
167 | -webkit-font-smoothing: antialiased;
168 | -moz-osx-font-smoothing: grayscale;
169 | text-rendering: optimizelegibility;
170 | }
171 |
172 | a {
173 | color: inherit;
174 | text-decoration: none;
175 | }
176 | button {
177 | appearance: none;
178 | background: none;
179 | border: none;
180 | color: inherit;
181 | cursor: pointer;
182 | padding: inherit;
183 | user-select: none;
184 | }
185 | ul,
186 | ol {
187 | margin: 0;
188 | padding: 0;
189 | }
190 |
191 | /* https://www.joshwcomeau.com/css/custom-css-reset/ */
192 | input,
193 | button,
194 | textarea,
195 | select {
196 | font: inherit;
197 | }
198 |
199 | /*
200 | ============================================================
201 | * HOME
202 | ============================================================
203 | */
204 |
205 | .hero-brand-mark--sparkle {
206 | transform-origin: 75% 33.33%;
207 | }
208 | .hero-brand-mark:hover {
209 | animation: brandMark 900ms ease;
210 | }
211 | .hero-brand-mark:hover .hero-brand-mark--sparkle {
212 | animation: brandMarkSparkle 900ms ease;
213 | }
214 |
215 | @keyframes brandMark {
216 | 10% {
217 | transform: skew(-1deg, 1deg);
218 | }
219 | 60%,
220 | 65% {
221 | transform: skew(5deg, -5deg);
222 | }
223 | }
224 | @keyframes brandMarkSparkle {
225 | 10% {
226 | transform: scale(0.8);
227 | }
228 | 60%,
229 | 65% {
230 | transform: scale(1.5) rotate(180deg);
231 | filter: drop-shadow(0 0 4px rgb(255 255 255 / 0.85));
232 | }
233 | }
234 |
235 | /*
236 | ============================================================
237 | * HIDDEN
238 | ============================================================
239 | */
240 |
241 | @media screen and (min-width: 601px) {
242 | .hidden-above-mobile {
243 | display: none !important;
244 | }
245 | }
246 | @media screen and (min-width: 1001px) {
247 | .hidden-above-tablet {
248 | display: none !important;
249 | }
250 | }
251 | @media screen and (max-width: 600px) {
252 | .hidden-below-mobile {
253 | display: none !important;
254 | }
255 | }
256 | @media screen and (max-width: 1000px) {
257 | .hidden-below-tablet {
258 | display: none !important;
259 | }
260 | }
261 |
262 | /*
263 | ============================================================
264 | * ARTICLE (PROSE)
265 | ============================================================
266 | */
267 |
268 | main > article {
269 | flex-grow: 1;
270 | line-height: 1.5;
271 | max-width: 100%;
272 | min-width: 0;
273 | padding: var(--gutter) var(--gutter) var(--gutter-large);
274 | }
275 |
276 | article hr {
277 | background-color: var(--border-prominent);
278 | height: 2px;
279 | border: 0;
280 | margin-bottom: var(--vertical-rhythm);
281 | }
282 |
283 | article img {
284 | max-width: 100%;
285 | }
286 |
287 | article a {
288 | color: var(--text-prominent);
289 | font-weight: var(--fw-medium);
290 | text-decoration: underline;
291 | text-decoration-color: var(--blue);
292 | text-decoration-thickness: 1px;
293 | text-underline-offset: 1px;
294 | transition: text-decoration 300ms ease;
295 | }
296 | article a:hover {
297 | text-decoration-thickness: 2px;
298 | }
299 | article a[href^='http'] {
300 | color: var(--blue); /* Indicate external links */
301 | }
302 |
303 | /* Vertical rhythm for block elements */
304 | article blockquote,
305 | article hr,
306 | article ol,
307 | article p,
308 | article table,
309 | article ul {
310 | margin-block: var(--vertical-rhythm);
311 | }
312 |
313 | article ol,
314 | article ul {
315 | margin-inline-start: var(--gutter);
316 | }
317 |
318 | /* Large blockquote treatment */
319 | article blockquote {
320 | color: var(--text-muted);
321 | font-size: 1.25rem;
322 | border-left: 4px solid var(--border);
323 | margin: var(--gutter);
324 | padding: 0 0 0 1rem;
325 | }
326 | blockquote p:last-child {
327 | margin-bottom: 0;
328 | }
329 |
330 | /*
331 | ============================================================
332 | * SYNTAX HIGHLIGHTING
333 | ============================================================
334 | */
335 |
336 | code {
337 | background: var(--surface);
338 | border-radius: var(--radii-small);
339 | font-family: var(--ff-mono);
340 | font-size: var(--fs-small);
341 | font-variant-ligatures: none;
342 | }
343 | :not(pre) > code {
344 | border: 1px solid var(--border-prominent);
345 | padding: 0.02rem 0.2rem;
346 | white-space: nowrap;
347 | }
348 |
349 | pre[class*='language-'] {
350 | background: var(--surface);
351 | border-radius: var(--radii-medium);
352 | color: var(--text);
353 | overflow-x: auto;
354 | display: flex;
355 | font-size: var(--fs-standard);
356 | padding: 0.8rem 1rem;
357 | text-shadow: none;
358 | text-align: left;
359 | white-space: pre;
360 | word-spacing: normal;
361 | word-break: normal;
362 | word-wrap: normal;
363 | line-height: 1.5;
364 | tab-size: 4;
365 | text-size-adjust: none;
366 | hyphens: none;
367 | }
368 | pre > code {
369 | font-size: 0.85em; /* preformatted text does weird stuff on mobile safari... */
370 | }
371 |
372 | .token.comment,
373 | .token.punctuation {
374 | color: var(--text-dim);
375 | }
376 |
377 | .token.symbol,
378 | .token.deleted,
379 | .token.important {
380 | color: var(--critical);
381 | }
382 |
383 | .token.important,
384 | .token.bold {
385 | font-weight: bold;
386 | }
387 |
388 | .token.italic {
389 | font-style: italic;
390 | }
391 |
392 | .token.property,
393 | .token.keyword,
394 | .token.function,
395 | .token.class-name {
396 | color: var(--blue);
397 | }
398 |
399 | .token.constant,
400 | .token.boolean,
401 | .token.number {
402 | color: var(--magenta);
403 | }
404 | .token.string {
405 | color: var(--yellow);
406 | }
407 |
408 | .token.builtin,
409 | .token.keyword {
410 | color: var(--purple);
411 | }
412 | .token.operator,
413 | .token.attr-name,
414 | .token.variable {
415 | color: var(--green);
416 | }
417 |
--------------------------------------------------------------------------------