vowel 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.js +5 -8
- package/package.json +2 -1
- package/plugins/atproto/body.js +20 -4
- package/plugins/html/bundle/index.js +4 -2
- package/plugins/html/bundle/live-reload.js +153 -0
- package/plugins/html/editor/ingest.js +7 -0
- package/plugins/html/editor/live-reload.js +8 -0
- package/plugins/html/editor/rollup.config.js +20 -8
- package/plugins/html/externalLinks.js +40 -0
- package/plugins/html/headingIds.js +35 -0
- package/plugins/html/index.js +30 -14
- package/plugins/html/markdownGrammar.js +13 -6
- package/plugins/html/partials.js +71 -21
- package/plugins/html/socialLinks.js +4 -1
- package/plugins/html/writeRules.js +52 -5
- package/plugins/markdown/codeFence.js +56 -0
- package/plugins/markdown/index.js +12 -1
- package/plugins/markdown/typography.js +5 -5
- package/stylesheets/DefaultStyles.css +28 -4
- package/stylesheets/SyntaxHighlightingStyles.css +65 -117
- package/stylesheets/TypographyStyles.css +15 -6
- package/version.js +9 -0
package/plugins/html/partials.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
2
|
import { h } from "hastscript"
|
|
3
|
-
import { isListedPage } from "../../utils.js"
|
|
3
|
+
import { isListedPage, pageClasses } from "../../utils.js"
|
|
4
4
|
import { isVowelStylesheet } from "../styles/theme.js"
|
|
5
5
|
|
|
6
6
|
/** The one stylesheet name linked without being asked for. */
|
|
@@ -96,8 +96,26 @@ function chain(folder) {
|
|
|
96
96
|
return ["", ...segments.map((_, i) => segments.slice(0, i + 1).join(path.sep))]
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
/**
|
|
100
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The class a nav's `<li>` carries (per Sam, Nov 7): the page class of
|
|
101
|
+
* the page it links to, the same as that page's `<body>` and cards
|
|
102
|
+
* (`page_blog_hello-world`).
|
|
103
|
+
* @param {any} page
|
|
104
|
+
*/
|
|
105
|
+
const pageClass = (page) => pageClasses(page.metadata.prettyURL, page.source).at(-1)
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The class a nav's `<ul>` carries: the folder its pages are in, as
|
|
109
|
+
* their own classes name it (`folder_blog`), or null for the root's.
|
|
110
|
+
* @param {any[]} pages - one folder's
|
|
111
|
+
*/
|
|
112
|
+
const folderClass = (pages) => {
|
|
113
|
+
const classes = pages.length ? pageClasses(pages[0].metadata.prettyURL, pages[0].source) : []
|
|
114
|
+
return classes.length > 1 ? classes.at(-2) : null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A nav entry: where it goes, what it says, and its page's class. */
|
|
118
|
+
const item = (page) => ({ url: page.metadata.prettyURL, label: page.metadata.breadcrumb, page: pageClass(page) })
|
|
101
119
|
|
|
102
120
|
/**
|
|
103
121
|
* The aside tree: top-level pages with no date, each with the pages of
|
|
@@ -118,16 +136,14 @@ function asideTree(pages) {
|
|
|
118
136
|
byFolder.get(url).push(page)
|
|
119
137
|
}
|
|
120
138
|
// A folder with too many pages for a menu shows its own entry only (asMenu).
|
|
121
|
-
const children = (parent) =>
|
|
122
|
-
const pages = byFolder.get(parent.metadata.prettyURL) ?? []
|
|
123
|
-
return asMenu(pages).map(node)
|
|
124
|
-
}
|
|
139
|
+
const children = (parent) => asMenu(byFolder.get(parent.metadata.prettyURL) ?? [])
|
|
125
140
|
|
|
126
141
|
function node(page) {
|
|
127
142
|
const below = children(page)
|
|
128
143
|
// The aside links the file, not the pretty url - kept as it was
|
|
129
144
|
// (see the note in the commit).
|
|
130
|
-
|
|
145
|
+
const entry = { url: "/" + page.path, label: page.metadata.breadcrumb, page: pageClass(page) }
|
|
146
|
+
return below.length ? { ...entry, children: below.map(node), folder: folderClass(below) } : entry
|
|
131
147
|
}
|
|
132
148
|
|
|
133
149
|
const top = undated
|
|
@@ -241,7 +257,8 @@ function createPartialStubs(api) {
|
|
|
241
257
|
// it, a folder with too many pages shows none of them (asMenu).
|
|
242
258
|
levels: chain(folder).map(level => {
|
|
243
259
|
const list = lists.get(level) ?? []
|
|
244
|
-
|
|
260
|
+
const shown = level ? asMenu(list) : list
|
|
261
|
+
return { folder: folderClass(shown), items: shown.map(item) }
|
|
245
262
|
}),
|
|
246
263
|
sheets: chain(folder).flatMap(level => sheets.get(level) ?? [])
|
|
247
264
|
}
|
|
@@ -255,20 +272,30 @@ function expandPartial({ params }) {
|
|
|
255
272
|
return { text: JSON.stringify(params) }
|
|
256
273
|
}
|
|
257
274
|
|
|
258
|
-
/**
|
|
275
|
+
/**
|
|
276
|
+
* The header nav: one <ul> per level that has entries, root first.
|
|
277
|
+
* Labelled "Local" and the aside "Global", after the settings that
|
|
278
|
+
* leave a page out of each (`local_nav_item`, `global_nav_item`), so a
|
|
279
|
+
* screen reader can tell the page's navs apart. The labels are CSS
|
|
280
|
+
* hooks too, like `nav[aria-label=Breadcrumbs]`: renaming one breaks
|
|
281
|
+
* an author's stylesheet.
|
|
282
|
+
*/
|
|
259
283
|
function renderNav({ levels }) {
|
|
260
|
-
return h("nav", levels
|
|
261
|
-
.filter(level => level.length)
|
|
262
|
-
.map(level => h("ul", level.map(({ url, label }) => h("li", h("a", { href: url }, label)))))
|
|
284
|
+
return h("nav", { "aria-label": "Local" }, levels
|
|
285
|
+
.filter(level => level.items.length)
|
|
286
|
+
.map(level => h("ul", classed(level.folder), level.items.map(({ url, label, page }) => h("li", classed(page), h("a", { href: url }, label)))))
|
|
263
287
|
)
|
|
264
288
|
}
|
|
265
289
|
|
|
290
|
+
/** One class, or none. @param {string | null | undefined} name */
|
|
291
|
+
const classed = (name) => name ? { className: [name] } : {}
|
|
292
|
+
|
|
266
293
|
/** The aside tree. */
|
|
267
294
|
function renderAside({ items }) {
|
|
268
|
-
const li = ({ url, label, children }) => children
|
|
269
|
-
? h("li", [h("a", { href: url }, label), h("ul", children.map(li))])
|
|
270
|
-
: h("li", h("a", { href: url }, label))
|
|
271
|
-
return h("nav", h("ul", items.map(li)))
|
|
295
|
+
const li = ({ url, label, page, children, folder }) => children
|
|
296
|
+
? h("li", classed(page), [h("a", { href: url }, label), h("ul", classed(folder), children.map(li))])
|
|
297
|
+
: h("li", classed(page), h("a", { href: url }, label))
|
|
298
|
+
return h("nav", { "aria-label": "Global" }, h("ul", items.map(li)))
|
|
272
299
|
}
|
|
273
300
|
|
|
274
301
|
/** "Linked from", outside section#content: it is generated, and the editor must not see it. */
|
|
@@ -294,16 +321,39 @@ function readPartial(source) {
|
|
|
294
321
|
}
|
|
295
322
|
|
|
296
323
|
/**
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
324
|
+
* A nav link's href as the page url it names: the aside links files
|
|
325
|
+
* (`/blog.html`, and `/index.html` for the home page), the header nav
|
|
326
|
+
* pretty urls.
|
|
327
|
+
* @param {string} href
|
|
328
|
+
*/
|
|
329
|
+
const linkedURL = (href) => href === "/index.html" ? "/" : href.replace(/\.html$/, "")
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* The urls of the folders above a page, not the root: `/blog` and
|
|
333
|
+
* `/blog/2020` for `/blog/2020/post`. Each is its folder's index page.
|
|
334
|
+
* @param {string} url
|
|
335
|
+
*/
|
|
336
|
+
function ancestorURLs(url) {
|
|
337
|
+
const segments = url.split("/").filter(Boolean)
|
|
338
|
+
return segments.slice(0, -1).map((segment, index) => "/" + segments.slice(0, index + 1).join("/"))
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The page's copy of a nav, with the current page marked
|
|
343
|
+
* `aria-current=page` and the folders above it `aria-current=true`
|
|
344
|
+
* (per Sam, Nov 7), the home page aside, since every page is below it.
|
|
345
|
+
* A copy: the tracked read hands back a fresh parse, but a rule that
|
|
346
|
+
* relies on that would be fragile, and the walk mutates.
|
|
300
347
|
* @param {any} hast
|
|
301
348
|
* @param {string} url - the page's prettyURL
|
|
302
349
|
*/
|
|
303
350
|
function withCurrent(hast, url) {
|
|
304
351
|
const copy = structuredClone(hast)
|
|
352
|
+
const ancestors = new Set(ancestorURLs(url))
|
|
305
353
|
const walk = (node) => {
|
|
306
|
-
|
|
354
|
+
const href = node.type === "element" && node.tagName === "a" && typeof node.properties?.href === "string" ? linkedURL(node.properties.href) : null
|
|
355
|
+
if (href === url) node.properties.ariaCurrent = "page"
|
|
356
|
+
else if (href !== null && ancestors.has(href)) node.properties.ariaCurrent = "true"
|
|
307
357
|
node.children?.forEach(walk)
|
|
308
358
|
}
|
|
309
359
|
walk(copy)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { h } from "hastscript"
|
|
2
2
|
import { imageSizes } from "../../utils.js"
|
|
3
3
|
import { imageURL } from "./image.js"
|
|
4
|
+
import { isExternalURL, externalProperties } from "./externalLinks.js"
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Social links, from the `social_links` setting:
|
|
@@ -52,6 +53,7 @@ function socialLinks(settings) {
|
|
|
52
53
|
function socialLinksNav(settings, api) {
|
|
53
54
|
const links = socialLinks(settings)
|
|
54
55
|
if (!links.length) return null
|
|
56
|
+
const domain = settings?.lastNonNull?.("fm_domain")
|
|
55
57
|
|
|
56
58
|
return h("nav.social-links", { "aria-label": "Social links" },
|
|
57
59
|
h("ul", links.map(({ label, url, icon }) => {
|
|
@@ -61,7 +63,8 @@ function socialLinksNav(settings, api) {
|
|
|
61
63
|
// published (image.js).
|
|
62
64
|
const iconURL = icon ? imageURL(String(icon), api, imageSizes[0]) : null
|
|
63
65
|
const image = iconURL && h("img", { src: iconURL, alt: text, style: `--url-icon: url("${iconURL}")` })
|
|
64
|
-
|
|
66
|
+
const properties = { href: url, rel: "me" }
|
|
67
|
+
return h("li", h("a", isExternalURL(url, domain) ? externalProperties(properties) : properties, image || text))
|
|
65
68
|
}))
|
|
66
69
|
)
|
|
67
70
|
}
|
|
@@ -8,6 +8,8 @@ import { globClasses, alertLabel } from "./editor/directives.js"
|
|
|
8
8
|
import createDynamicImage, { derivativeURL } from "./image.js"
|
|
9
9
|
import { highlightCode } from "./highlighting.js"
|
|
10
10
|
import { displayPath } from "../../secretPaths.js"
|
|
11
|
+
import { isExternalURL, externalProperties } from "./externalLinks.js"
|
|
12
|
+
import { HEADING_ID_PREFIX, headingAnchor, withHeadingAnchor } from "./headingIds.js"
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* The page's source, at the node a warning is about (utils.js). Nodes
|
|
@@ -148,8 +150,39 @@ const internalLink = {
|
|
|
148
150
|
return
|
|
149
151
|
}
|
|
150
152
|
// A link to a png or jpeg opens its largest derivative: the
|
|
151
|
-
// original is not published (image.js).
|
|
152
|
-
|
|
153
|
+
// original is not published (image.js). A page's anchor is a
|
|
154
|
+
// heading's (headingIds.js); a file's, like a pdf's `#page=2`, isn't.
|
|
155
|
+
const anchored = found.extension === ".html" ? withHeadingAnchor(suffix) : suffix
|
|
156
|
+
node.properties.href = (derivativeURL(found, imageSizes.at(-1)) ?? urlOf(found)) + anchored
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* `[x](#kerning)`, an anchor on this page: a heading's, so `#heading_kerning`
|
|
162
|
+
* (headingIds.js). In place, so the walk goes on into its text.
|
|
163
|
+
* @type {import("../markdown/visitor.js").BlockRule}
|
|
164
|
+
*/
|
|
165
|
+
const anchorLink = {
|
|
166
|
+
name: "anchorLink",
|
|
167
|
+
test: (node) => isElement(node) && node.tagName === "a" && typeof node.properties?.href === "string" && node.properties.href.startsWith("#"),
|
|
168
|
+
transform: (node) => {
|
|
169
|
+
node.properties.href = "#" + headingAnchor(node.properties.href.slice(1))
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A link to another site opens in a new tab (externalLinks.js). Any
|
|
175
|
+
* absolute link matches; one to the site's own domain is left as it
|
|
176
|
+
* is, which a test can't tell, since only the transform has the
|
|
177
|
+
* settings. In place, so the walk goes on into its text.
|
|
178
|
+
* @type {import("../markdown/visitor.js").BlockRule}
|
|
179
|
+
*/
|
|
180
|
+
const externalLink = {
|
|
181
|
+
name: "externalLink",
|
|
182
|
+
test: (node) => isElement(node) && node.tagName === "a" && /^(https?:)?\/\//i.test(node.properties?.href ?? ""),
|
|
183
|
+
transform: (node, index, parent, { settings }) => {
|
|
184
|
+
if (!isExternalURL(node.properties.href, settings?.lastNonNull?.("fm_domain"))) return
|
|
185
|
+
node.properties = externalProperties(node.properties)
|
|
153
186
|
}
|
|
154
187
|
}
|
|
155
188
|
|
|
@@ -247,7 +280,7 @@ const wikilink = {
|
|
|
247
280
|
return SKIP
|
|
248
281
|
}
|
|
249
282
|
const url = found.metadata?.prettyURL ?? `/${found.path}`
|
|
250
|
-
const anchor = section ? `#${new GithubSlugger().slug(section)}` : ""
|
|
283
|
+
const anchor = section ? `#${HEADING_ID_PREFIX}${new GithubSlugger().slug(section)}` : ""
|
|
251
284
|
node.properties.href = url + anchor
|
|
252
285
|
return SKIP
|
|
253
286
|
}
|
|
@@ -323,6 +356,7 @@ const captionedImage = {
|
|
|
323
356
|
transform: (node) => {
|
|
324
357
|
const [img, ...caption] = node.children
|
|
325
358
|
node.tagName = "figure"
|
|
359
|
+
node.properties = { ...node.properties, className: [...(node.properties?.className ?? []), "image"] }
|
|
326
360
|
node.children = [img, h("figcaption", caption)]
|
|
327
361
|
}
|
|
328
362
|
}
|
|
@@ -419,7 +453,7 @@ const directive = {
|
|
|
419
453
|
// Lowercased: every vowel target path is (config.js's router).
|
|
420
454
|
const found = api.target(path.relative("/", path.format(info)).toLowerCase())
|
|
421
455
|
if (!found) return SKIP
|
|
422
|
-
return makeHeader(found.metadata, cardLink(found), api, config, found.source)
|
|
456
|
+
return asReference(makeHeader(found.metadata, cardLink(found), api, config, found.source))
|
|
423
457
|
}
|
|
424
458
|
|
|
425
459
|
const folder = path.relative("/", dir)
|
|
@@ -455,6 +489,19 @@ const directive = {
|
|
|
455
489
|
*/
|
|
456
490
|
const cardLink = (found) => isPageWithoutFile(found) ? null : found.metadata.prettyURL
|
|
457
491
|
|
|
492
|
+
/**
|
|
493
|
+
* A reference card's <article> marked `reference`, which a listing's
|
|
494
|
+
* cards aren't: `<a><article class=reference>`, or the article alone for
|
|
495
|
+
* a page with no file. The editor's ingest reads it to put `/blog/post`
|
|
496
|
+
* back.
|
|
497
|
+
* @param {any} card - makeHeader's result
|
|
498
|
+
*/
|
|
499
|
+
function asReference(card) {
|
|
500
|
+
const article = card.tagName === "article" ? card : card.children[0]
|
|
501
|
+
article.properties.className = [...(article.properties.className ?? []), "reference"]
|
|
502
|
+
return card
|
|
503
|
+
}
|
|
504
|
+
|
|
458
505
|
/**
|
|
459
506
|
* A relative directive path as the absolute published path it names.
|
|
460
507
|
* The glob's last segment (`*`, `**`) rides along untouched: the
|
|
@@ -569,7 +616,7 @@ function trimText(nodes) {
|
|
|
569
616
|
const skipped = new Set(["pre", "code", "script", "style"])
|
|
570
617
|
|
|
571
618
|
const writeWalk = createVisitor({
|
|
572
|
-
block: [internalLink, wikilink, icon, highlight, captionedImage, image, directive, preview, alert],
|
|
619
|
+
block: [internalLink, anchorLink, externalLink, wikilink, icon, highlight, captionedImage, image, directive, preview, alert],
|
|
573
620
|
skip: (node) => isElement(node) && skipped.has(node.tagName)
|
|
574
621
|
})
|
|
575
622
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fenced code block's info string: its language, then its meta, which
|
|
3
|
+
* can carry a title (per Sam, Nov 6):
|
|
4
|
+
*
|
|
5
|
+
* ```md "Hello World.md"
|
|
6
|
+
* ```js title="app.js"
|
|
7
|
+
*
|
|
8
|
+
* The title is `title="..."` when the meta has one, else the first
|
|
9
|
+
* quoted string standing on its own, in double or single quotes. The
|
|
10
|
+
* rest of the meta is the author's, as frontmatter keys vowel doesn't
|
|
11
|
+
* read are. GitHub ignores the meta, so a titled block still renders
|
|
12
|
+
* there, without its title. `title="..."` is Docusaurus', Astro's and
|
|
13
|
+
* rehype-pretty-code's spelling; the bare string is Vowel's short form,
|
|
14
|
+
* which rules out Expressive Code's meaning for one (mark this text).
|
|
15
|
+
*
|
|
16
|
+
* Free of node builtins: the editor may need it.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const KEYED = /(?:^|\s)title=(?:"([^"]*)"|'([^']*)')(?=\s|$)/
|
|
20
|
+
const BARE = /(?:^|\s)(?:"([^"]*)"|'([^']*)')(?=\s|$)/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The fence as micromark split it, at the first whitespace, read again.
|
|
24
|
+
* A quoted first word is not a language: in ```` ```"notes.txt" ````
|
|
25
|
+
* micromark's language is `"notes.txt"`, and the block has none.
|
|
26
|
+
* @param {string | null | undefined} lang - mdast's `code.lang`
|
|
27
|
+
* @param {string | null | undefined} meta - mdast's `code.meta`
|
|
28
|
+
* @returns {{ language: string | null, meta: string | null, title: string | undefined }}
|
|
29
|
+
*/
|
|
30
|
+
export function parseFence(lang, meta) {
|
|
31
|
+
const quoted = /^["']/.test(lang ?? "")
|
|
32
|
+
const language = lang && !quoted ? lang : null
|
|
33
|
+
const rest = (quoted ? [lang, meta].filter(Boolean).join(" ") : meta) || null
|
|
34
|
+
const match = rest ? KEYED.exec(rest) ?? BARE.exec(rest) : null
|
|
35
|
+
const title = (match?.[1] ?? match?.[2])?.trim() || undefined
|
|
36
|
+
return { language, meta: rest, title }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A title written back into a fence, as mdast's `lang` and `meta`:
|
|
41
|
+
* `title="..."` after a language, the spelling other tools read, and
|
|
42
|
+
* the bare string without one, since a `title=` would be read as the
|
|
43
|
+
* language. Split at the first space, as micromark splits it: a `lang`
|
|
44
|
+
* with a space in it is written with the space escaped. A title with a
|
|
45
|
+
* double quote in it is in single quotes.
|
|
46
|
+
* @param {string | null} language
|
|
47
|
+
* @param {string} title
|
|
48
|
+
* @returns {{ lang: string | null, meta: string | null }}
|
|
49
|
+
*/
|
|
50
|
+
export function fenceWithTitle(language, title) {
|
|
51
|
+
const quote = title.includes('"') ? "'" : '"'
|
|
52
|
+
const quotedTitle = `${quote}${title}${quote}`
|
|
53
|
+
if (language) return { lang: language, meta: `title=${quotedTitle}` }
|
|
54
|
+
const [first, ...rest] = quotedTitle.split(/\s+/)
|
|
55
|
+
return { lang: first, meta: rest.join(" ") || null }
|
|
56
|
+
}
|
|
@@ -20,6 +20,7 @@ import { toString as hastToString } from 'hast-util-to-string'
|
|
|
20
20
|
import getMetadata, { imageFromPage, reservedProperties } from "./metadata.js"
|
|
21
21
|
import { isSecretPath, secretRouter, displayPath } from "./../../secretPaths.js"
|
|
22
22
|
import { collectLinks } from "./links.js"
|
|
23
|
+
import { parseFence } from "./codeFence.js"
|
|
23
24
|
import { readWalk } from "./readRules.js"
|
|
24
25
|
import { h } from "hastscript"
|
|
25
26
|
import { mirrorStubs, mirrorWriter, llmsStubs } from "./mirror.js"
|
|
@@ -146,8 +147,18 @@ function readFile(source, { api, config }) {
|
|
|
146
147
|
// is too wide, so the table itself can be a real table at full width
|
|
147
148
|
// (a table that scrolls has to be display: block, which sizes its
|
|
148
149
|
// columns to their content), and a caption has somewhere to go.
|
|
150
|
+
// A code block with a title (codeFence.js) is in one too, the title
|
|
151
|
+
// as its caption. One without stays a bare <pre>.
|
|
149
152
|
handlers: {
|
|
150
|
-
table: (state, node) => h("figure", [defaultHandlers.table(state, node)])
|
|
153
|
+
table: (state, node) => h("figure.table", [defaultHandlers.table(state, node)]),
|
|
154
|
+
code: (state, node) => {
|
|
155
|
+
const fence = parseFence(node.lang, node.meta)
|
|
156
|
+
const pre = defaultHandlers.code(state, { ...node, lang: fence.language, meta: fence.meta })
|
|
157
|
+
if (!fence.title) return pre
|
|
158
|
+
const figure = h("figure.code", [h("figcaption", fence.title), pre])
|
|
159
|
+
state.patch(node, figure)
|
|
160
|
+
return figure
|
|
161
|
+
}
|
|
151
162
|
},
|
|
152
163
|
// `state.all(node)` converts the children to hast; handing over
|
|
153
164
|
// `node.children` gives the HTML compiler raw *mdast* instead. Plain
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* @property {string} variable - the custom property in TypographyStyles.css
|
|
21
21
|
* @property {string} label
|
|
22
22
|
* @property {string} [hint]
|
|
23
|
-
* @property {"
|
|
23
|
+
* @property {"ch" | ""} unit
|
|
24
24
|
* @property {number} min
|
|
25
25
|
* @property {number} max
|
|
26
26
|
* @property {number} step
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
/** @type {TypographySetting[]} */
|
|
31
31
|
export const rampSettings = [
|
|
32
|
-
{ key: "heading-size", variable: "--fontsize-
|
|
32
|
+
{ key: "heading-size", variable: "--fontsize-scale", label: "Heading size", hint: "How much larger h1 is than body text, as a multiple of it", unit: "", min: 0.25, max: 4, step: 0.05, default: 1.5 },
|
|
33
33
|
{ key: "heading-size-decay", variable: "--fontsize-decay", label: "Size decay", hint: "Higher makes subheadings smaller", unit: "", min: 0.5, max: 6, step: 0.1, default: 3 },
|
|
34
34
|
{ key: "h1-letter-spacing", variable: "--letterspacing-h1", label: "h1 letter spacing", unit: "ch", min: -0.1, max: 0.1, step: 0.005, default: -0.01 },
|
|
35
35
|
{ key: "h6-letter-spacing", variable: "--letterspacing-h6", label: "h6 letter spacing", unit: "ch", min: -0.1, max: 0.1, step: 0.005, default: 0 },
|
|
@@ -49,15 +49,15 @@ function themeSettings(theme) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* A ramp setting's value as CSS, clamped to its range.
|
|
53
|
-
*
|
|
52
|
+
* A ramp setting's value as CSS, clamped to its range. `heading-size` is
|
|
53
|
+
* a plain number, `--fontsize-scale`, which the sheet multiplies by the
|
|
54
|
+
* root size, so headings follow the root's clamp() up and down.
|
|
54
55
|
* @param {TypographySetting} setting
|
|
55
56
|
* @param {number} value
|
|
56
57
|
*/
|
|
57
58
|
function rampValue(setting, value) {
|
|
58
59
|
const clamped = Math.min(setting.max, Math.max(setting.min, value))
|
|
59
60
|
const number = String(Number(clamped.toFixed(4)))
|
|
60
|
-
if (setting.unit === "x") return `calc(${number} * var(--fontsize-root))`
|
|
61
61
|
return `${number}${setting.unit}`
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -66,10 +66,29 @@
|
|
|
66
66
|
--color-text-hover-panel: light-dark(var(--color-primary-02), var(--color-primary-08));
|
|
67
67
|
--color-border-hover-panel: light-dark(var(--color-accent-08), var(--color-accent-05));
|
|
68
68
|
|
|
69
|
-
/*
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
/*
|
|
70
|
+
pre colors, and the syntax-highlighting sheet's from the
|
|
71
|
+
palette: the text at the far end from the background, every
|
|
72
|
+
token one shade in, so each stays above 4.5:1 on the
|
|
73
|
+
background for any ordinary palette (tests/syntaxColors.js).
|
|
74
|
+
Comments too, told apart by italics: a shade lighter fell to
|
|
75
|
+
4.2:1 on a palette from bright colours (#ffcc00, #66ff00).
|
|
76
|
+
*/
|
|
77
|
+
--color-background-pre: light-dark(var(--color-tertiary-10), var(--color-tertiary-02));
|
|
78
|
+
--color-border-pre: light-dark(var(--color-tertiary-08), var(--color-tertiary-02));
|
|
79
|
+
--color-text-pre: light-dark(var(--color-tertiary-04), var(--color-tertiary-08));
|
|
80
|
+
--color-comment-pre: light-dark(var(--color-accent-04), var(--color-accent-09));
|
|
81
|
+
--color-keyword-pre: light-dark(var(--color-primary-04), var(--color-primary-07));
|
|
82
|
+
--color-function-pre: light-dark(var(--color-secondary-04), var(--color-secondary-07));
|
|
83
|
+
--color-constant-pre: light-dark(var(--color-info-04), var(--color-info-07));
|
|
84
|
+
--color-string-pre: light-dark(var(--color-tertiary-04), var(--color-tertiary-08));
|
|
85
|
+
--color-builtin-pre: light-dark(var(--color-warning-04), var(--color-warning-07));
|
|
86
|
+
--color-tag-pre: light-dark(var(--color-success-05), var(--color-success-07));
|
|
87
|
+
--color-list-pre: light-dark(var(--color-warning-05), var(--color-warning-07));
|
|
88
|
+
--color-inserted-pre: light-dark(var(--color-success-05), var(--color-success-07));
|
|
89
|
+
--color-background-inserted-pre: light-dark(var(--color-success-07), var(--color-success-05));
|
|
90
|
+
--color-deleted-pre: light-dark(var(--color-danger-05), var(--color-danger-07));
|
|
91
|
+
--color-background-deleted-pre: light-dark(var(--color-danger-07), var(--color-danger-05));
|
|
73
92
|
|
|
74
93
|
/*
|
|
75
94
|
alert colors: --color-{element}-alert-{type} for the types the
|
|
@@ -484,6 +503,11 @@
|
|
|
484
503
|
background: var(--color-background-pre);
|
|
485
504
|
}
|
|
486
505
|
|
|
506
|
+
/* a comment is the text's colour, a shade lighter: italics tell it apart */
|
|
507
|
+
.token.comment {
|
|
508
|
+
font-style: italic;
|
|
509
|
+
}
|
|
510
|
+
|
|
487
511
|
blockquote {
|
|
488
512
|
border-left: 0.5ch solid var(--color-border-body);
|
|
489
513
|
}
|