slidev-theme-tud-db 0.0.7 → 0.0.9
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/README.md +6 -4
- package/package.json +1 -1
- package/setup/shiki.ts +85 -11
- package/styles/db-theme.css +18 -3
- package/vite.config.ts +56 -15
package/README.md
CHANGED
|
@@ -53,9 +53,11 @@ cp -r node_modules/slidev-theme-db/skills/slidev-theme-db .claude/skills/
|
|
|
53
53
|
- `components/DbFrame.vue` — shared chrome (logo, footer, padding).
|
|
54
54
|
- `layouts/` — `cover`, `default`, `two-cols-header`, `two-cols-grid` (wrap `DbFrame`).
|
|
55
55
|
- `styles/db-theme.css` — tokens, typography, lists, tables (keyed on `.db-default`).
|
|
56
|
-
- `setup/shiki.ts`
|
|
57
|
-
the
|
|
58
|
-
`
|
|
59
|
-
(
|
|
56
|
+
- `setup/shiki.ts` — code highlighting: the SQL grammar (Shiki's, plus a few
|
|
57
|
+
rules) and the colour themes (catppuccin, upright, with SQL tweaks), built by
|
|
58
|
+
`highlighting()`. Fenced code uses them through Slidev; `vite.config.ts` uses
|
|
59
|
+
the same `highlighting()` for inline `{lang}` code, so the two never drift.
|
|
60
|
+
It also holds the transformer behind `⟦name:text⟧` code marks (spans a
|
|
61
|
+
slide's timeline can recolour).
|
|
60
62
|
- Link the theme with `link:` (not `file:`), and **restart** the dev server after
|
|
61
63
|
editing theme files (Vite doesn't hot-reload `node_modules`).
|
package/package.json
CHANGED
package/setup/shiki.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Shiki setup for fenced code blocks (```lang ... ```) and magic-move blocks.
|
|
2
|
+
// vite.config.ts highlights inline code with the same grammar and colours (see
|
|
3
|
+
// `highlighting()` below), so inline and fenced code look the same.
|
|
4
4
|
//
|
|
5
5
|
// NB: no `import { defineShikiSetup } from '@slidev/types'` on purpose — it's
|
|
6
6
|
// only a type helper, and Slidev calls this default export as `setup(ctx)`.
|
|
7
|
-
//
|
|
7
|
+
// The grammar and the colour themes are Shiki's own bundled ones, adjusted
|
|
8
|
+
// below, so no extra deps are needed.
|
|
9
|
+
import { bundledLanguages, bundledThemes } from 'shiki'
|
|
8
10
|
|
|
9
11
|
// Code marks: wrap exact character ranges of a fenced block so a slide can
|
|
10
12
|
// recolour that text, e.g. at a click. Inside the code, write ⟦name:text⟧: the
|
|
@@ -24,7 +26,7 @@ const MARK = /⟦([\w-]+):([^⟧\n]*)⟧/g
|
|
|
24
26
|
|
|
25
27
|
interface Decoration { start: number, end: number, properties: Record<string, string> }
|
|
26
28
|
|
|
27
|
-
const codeMarks = {
|
|
29
|
+
export const codeMarks = {
|
|
28
30
|
name: 'tud-db:code-marks',
|
|
29
31
|
preprocess(code: string, options: { decorations?: Decoration[] }) {
|
|
30
32
|
if (!code.includes('⟦'))
|
|
@@ -46,10 +48,82 @@ const codeMarks = {
|
|
|
46
48
|
},
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// SQL: Shiki's grammar, with a few rules added. Being grammar rules, they
|
|
52
|
+
// apply only where SQL code is parsed, never inside comments or strings.
|
|
53
|
+
// - NULL, TRUE, FALSE and UNKNOWN are constants (the grammar knows only NULL,
|
|
54
|
+
// as a keyword, and reads `IS NOT NULL` as a single keyword: here the
|
|
55
|
+
// keyword part and the constant are told apart).
|
|
56
|
+
// - EXPLAIN and ANALYZE are keywords.
|
|
57
|
+
// - A name right before `(` is a function call: `ceil(x)`, `jsonb_agg(…)`;
|
|
58
|
+
// so are common function names on their own, as in an inline `AVG` (the
|
|
59
|
+
// grammar knows functions only before `(`).
|
|
60
|
+
// These rules come last, so the grammar's own reading of a word, such as
|
|
61
|
+
// the keyword in `IN (` or the type in `DECIMAL(10, 2)`, takes precedence.
|
|
62
|
+
const CONSTANTS = 'NULL|TRUE|FALSE|UNKNOWN'
|
|
63
|
+
const FUNCTIONS = 'COUNT|AVG|SUM|MIN|MAX|COALESCE|ROUND|ABS|CEIL|FLOOR|SUBSTRING|TRIM|UPPER|LOWER|CONCAT'
|
|
64
|
+
const SQL_RULES_FIRST = [
|
|
65
|
+
{
|
|
66
|
+
match: `(?i:\\b(IS(?:\\s+NOT)?|NOT)\\s+(${CONSTANTS})\\b)`,
|
|
67
|
+
captures: { 1: { name: 'keyword.other.sql' }, 2: { name: 'constant.language.sql' } },
|
|
53
68
|
},
|
|
54
|
-
|
|
55
|
-
}
|
|
69
|
+
{ match: `(?i:\\b(?:${CONSTANTS})\\b)`, name: 'constant.language.sql' },
|
|
70
|
+
{ match: '(?i:\\b(?:EXPLAIN|ANALYZE)\\b)', name: 'keyword.other.sql' },
|
|
71
|
+
]
|
|
72
|
+
const SQL_RULES_LAST = [
|
|
73
|
+
{ match: '\\b[A-Za-z_][A-Za-z0-9_]*(?=\\()', name: 'support.function.sql' },
|
|
74
|
+
{ match: `(?i:\\b(?:${FUNCTIONS})\\b)`, name: 'support.function.sql' },
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
// Colours: catppuccin-latte (light) and -mocha (dark), upright throughout (the
|
|
78
|
+
// template's code has no italics; Shiki's catppuccin sets some, e.g. on
|
|
79
|
+
// function names), with two SQL changes: numbers green, so that they differ
|
|
80
|
+
// from the orange constants (catppuccin colours both orange); and the table
|
|
81
|
+
// name after CREATE TABLE plain text, not a blue function name.
|
|
82
|
+
interface ThemeRule { scope?: string | string[], settings: { foreground?: string, fontStyle?: string } }
|
|
83
|
+
interface Theme { name: string, tokenColors?: ThemeRule[], [key: string]: unknown }
|
|
84
|
+
|
|
85
|
+
function variant(base: Theme, name: string, c: { constant: string, number: string, text: string }): Theme {
|
|
86
|
+
const upright = (base.tokenColors ?? []).map((rule) => {
|
|
87
|
+
const style = rule.settings.fontStyle
|
|
88
|
+
if (!style?.includes('italic'))
|
|
89
|
+
return rule
|
|
90
|
+
return { ...rule, settings: { ...rule.settings, fontStyle: style.replace('italic', '').trim() } }
|
|
91
|
+
})
|
|
92
|
+
return {
|
|
93
|
+
...base,
|
|
94
|
+
name,
|
|
95
|
+
tokenColors: [
|
|
96
|
+
...upright,
|
|
97
|
+
{ scope: 'constant.language.sql', settings: { foreground: c.constant } },
|
|
98
|
+
{ scope: 'constant.numeric.sql', settings: { foreground: c.number } },
|
|
99
|
+
{ scope: 'entity.name.function.sql', settings: { foreground: c.text } },
|
|
100
|
+
],
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The theme's SQL grammar and its light and dark code colours (loaded once). */
|
|
105
|
+
export const highlighting = (() => {
|
|
106
|
+
let loaded: Promise<{ sql: any, light: Theme, dark: Theme }> | undefined
|
|
107
|
+
return () => loaded ??= (async () => {
|
|
108
|
+
const [[sql], latte, mocha] = await Promise.all([
|
|
109
|
+
bundledLanguages.sql().then(m => m.default as any[]),
|
|
110
|
+
bundledThemes['catppuccin-latte']().then(m => m.default as unknown as Theme),
|
|
111
|
+
bundledThemes['catppuccin-mocha']().then(m => m.default as unknown as Theme),
|
|
112
|
+
])
|
|
113
|
+
return {
|
|
114
|
+
sql: { ...sql, patterns: [...SQL_RULES_FIRST, ...sql.patterns, ...SQL_RULES_LAST] },
|
|
115
|
+
light: variant(latte, 'tud-db-light', { constant: '#fe640b', number: '#059669', text: '#4c4f69' }),
|
|
116
|
+
dark: variant(mocha, 'tud-db-dark', { constant: '#fab387', number: '#34d399', text: '#cdd6f4' }),
|
|
117
|
+
}
|
|
118
|
+
})()
|
|
119
|
+
})()
|
|
120
|
+
|
|
121
|
+
export default async () => {
|
|
122
|
+
const { sql, light, dark } = await highlighting()
|
|
123
|
+
return {
|
|
124
|
+
themes: { light, dark },
|
|
125
|
+
// replaces Shiki's `sql` with the extended grammar above
|
|
126
|
+
langs: [sql],
|
|
127
|
+
transformers: [codeMarks],
|
|
128
|
+
}
|
|
129
|
+
}
|
package/styles/db-theme.css
CHANGED
|
@@ -58,14 +58,21 @@
|
|
|
58
58
|
|
|
59
59
|
/* Inline code "pills": slimmer so they don't bloat line height in bullets.
|
|
60
60
|
(Slidev default is ~2px 6px / 0.9em.) Fenced code (pre > code) is untouched. */
|
|
61
|
-
.slidev-layout.db-default :not(pre) > code
|
|
61
|
+
.slidev-layout.db-default :not(pre) > code,
|
|
62
|
+
.slidev-layout :not(pre) > code,
|
|
63
|
+
code.db-inline-code {
|
|
62
64
|
padding: 1px 0; /* no horizontal padding by default so it sits flush with text */
|
|
63
65
|
font-size: 0.85em;
|
|
66
|
+
font-weight: 400; /* regular weight — override Slidev's thin 300 default */
|
|
64
67
|
line-height: 1;
|
|
65
68
|
background: transparent !important; /* transparent by default */
|
|
69
|
+
font-variant-ligatures: none;
|
|
70
|
+
white-space: pre-wrap;
|
|
66
71
|
}
|
|
67
72
|
/* Opt-in faint background for inline code */
|
|
68
|
-
.slidev-layout.db-default :not(pre) > code.with-bg
|
|
73
|
+
.slidev-layout.db-default :not(pre) > code.with-bg,
|
|
74
|
+
.slidev-layout :not(pre) > code.with-bg,
|
|
75
|
+
code.db-inline-code.with-bg {
|
|
69
76
|
padding: 1px 4px; /* add horizontal padding back for the background box */
|
|
70
77
|
background: rgba(0, 0, 0, 0.03) !important;
|
|
71
78
|
}
|
|
@@ -199,7 +206,7 @@
|
|
|
199
206
|
}
|
|
200
207
|
.slidev-layout.db-default .db-takeaway__body p { margin: 0; }
|
|
201
208
|
.slidev-layout.db-default .db-takeaway__body strong { font-weight: 700; }
|
|
202
|
-
.slidev-layout.db-default .db-takeaway__body code { font-weight:
|
|
209
|
+
.slidev-layout.db-default .db-takeaway__body code { font-weight: 400; }
|
|
203
210
|
|
|
204
211
|
/* Fenced code blocks: box only as wide as its content, not full slide width.
|
|
205
212
|
fit-content on the WRAPPER so an optional caption + the code share one width.
|
|
@@ -208,6 +215,14 @@
|
|
|
208
215
|
width: fit-content;
|
|
209
216
|
max-width: 100%;
|
|
210
217
|
}
|
|
218
|
+
/* Code that morphs to the next slide (data-morph, from the gsap addon): while
|
|
219
|
+
the morph animates its box from the other slide's size, the code can be
|
|
220
|
+
wider than the box for a moment, and Slidev's `overflow: auto` on code
|
|
221
|
+
blocks would flash a scrollbar. Let it overflow instead; it fits again once
|
|
222
|
+
the morph ends. */
|
|
223
|
+
.slidev-layout [data-morph] .slidev-code {
|
|
224
|
+
overflow: visible;
|
|
225
|
+
}
|
|
211
226
|
/* Default to transparent background for multi-line blocks, overriding Shiki */
|
|
212
227
|
.slidev-layout.db-default pre.slidev-code {
|
|
213
228
|
background: transparent !important;
|
package/vite.config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHighlighter } from 'shiki'
|
|
2
|
-
import { setupTableCaptions } from './markdown'
|
|
2
|
+
import { setupTableCaptions } from './markdown.ts'
|
|
3
|
+
import { highlighting } from './setup/shiki.ts'
|
|
3
4
|
|
|
4
5
|
// Shipped WITH the theme: Slidev merges vite.config from every root (theme +
|
|
5
6
|
// addons + deck), so installing this theme is enough — no need to copy a file.
|
|
@@ -7,19 +8,40 @@ import { setupTableCaptions } from './markdown'
|
|
|
7
8
|
// Inline code syntax highlighting. Write inline code as: `{lang} the code`
|
|
8
9
|
// e.g. `{json} {"a": 1}`
|
|
9
10
|
// The leading `{lang}` token selects the language; everything after the first
|
|
10
|
-
// space is highlighted with Shiki and rendered inside a normal inline <code
|
|
11
|
+
// space is highlighted with Shiki and rendered inside a normal inline <code>,
|
|
12
|
+
// with the same SQL grammar and colours as fenced code (setup/shiki.ts).
|
|
11
13
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
+
// Untagged inline code is highlighted too when it is clearly one of:
|
|
15
|
+
// - SQL: it has an UPPERCASE SQL word (`NULL`, `GROUP BY`, `TRUE AND UNKNOWN`)
|
|
16
|
+
// or is a lone comparison operator (`<>`, `<=>`). Uppercase only, so that
|
|
17
|
+
// code from other languages (`std::set`, `end()`, `{lines:true}`) stays plain.
|
|
18
|
+
// - a function call, `next()` or `child.next()`: the name in function blue.
|
|
14
19
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
+
// A deck can still override LANGS or the rule by defining its own
|
|
21
|
+
// vite.config.ts (the deck's config merges last and wins).
|
|
22
|
+
const LANGS = ['json', 'yaml', 'c', 'cpp', 'python']
|
|
23
|
+
|
|
24
|
+
const AUTO_SQL = /\b(?:TRUE|FALSE|UNKNOWN|NULL|EXPLAIN|ANALYZE|COUNT|AVG|SUM|MIN|MAX|COALESCE|SELECT|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|JOIN|LEFT|RIGHT|INNER|OUTER|FULL|CROSS|ON|USING|LIMIT|OFFSET|UNION|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|TABLE|INTO|VALUES|SET|BETWEEN|IN|NOT|AND|OR|IS|LIKE|ILIKE|AS|CASE|WHEN|THEN|ELSE|END)\b/
|
|
25
|
+
const AUTO_SQL_OPS = /^(?:<=>|<>|<=|>=|!=|=|<|>)$/
|
|
26
|
+
const CALL = /^((?:[a-z_]\w*\.)*)([a-z_]\w*)(\s*\(.*\))$/i
|
|
27
|
+
|
|
28
|
+
const escapeHtml = (s: string) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
29
|
+
|
|
30
|
+
// Vue compiles each slide as a template and condenses whitespace: it drops a
|
|
31
|
+
// text node that is only spaces at the start or end of an element (Shiki's
|
|
32
|
+
// span for a lone space), and shrinks runs of spaces to one. Protect exactly
|
|
33
|
+
// those spaces with ; every other space stays a normal one, so long
|
|
34
|
+
// inline code can still wrap.
|
|
35
|
+
function keepSpaces(html: string): string {
|
|
36
|
+
return html
|
|
37
|
+
.replace(/>( +)</g, (_m, sp: string) => `>${' '.repeat(sp.length)}<`)
|
|
38
|
+
.replace(/ {2,}/g, sp => `${' '.repeat(sp.length - 1)} `)
|
|
39
|
+
}
|
|
20
40
|
|
|
21
41
|
export default async () => {
|
|
22
|
-
const
|
|
42
|
+
const { sql, light } = await highlighting()
|
|
43
|
+
const highlighter = await createHighlighter({ themes: [light], langs: [...LANGS, sql] })
|
|
44
|
+
const THEME = light.name
|
|
23
45
|
|
|
24
46
|
return {
|
|
25
47
|
slidev: {
|
|
@@ -29,17 +51,36 @@ export default async () => {
|
|
|
29
51
|
setupTableCaptions(md)
|
|
30
52
|
const fallback = md.renderer.rules.code_inline
|
|
31
53
|
md.renderer.rules.code_inline = (tokens: any[], idx: number, options: any, env: any, self: any) => {
|
|
32
|
-
const
|
|
33
|
-
|
|
54
|
+
const raw = String(tokens[idx].content)
|
|
55
|
+
const m = raw.match(/^\{([\w-]+)\}\s+([\s\S]+)$/)
|
|
56
|
+
const code = m ? m[2] : raw
|
|
57
|
+
const bare = code.trim()
|
|
58
|
+
const lang = m ? m[1] : (AUTO_SQL_OPS.test(bare) || AUTO_SQL.test(bare)) ? 'sql' : null
|
|
59
|
+
|
|
60
|
+
// untagged function call: the name in function blue, the rest in
|
|
61
|
+
// the colour of punctuation (as Shiki colours `()` in code)
|
|
62
|
+
const call = !lang && bare.match(CALL)
|
|
63
|
+
if (call) {
|
|
64
|
+
const [, prefix, name, rest] = call
|
|
65
|
+
const span = (color: string, text: string) => text ? `<span style="color:${color}">${escapeHtml(text)}</span>` : ''
|
|
66
|
+
const inner = span('#4C4F69', prefix) + span('#1E66F5', name) + span('#7C7F93', rest)
|
|
67
|
+
return `<code class="db-inline-code">${keepSpaces(inner)}</code>`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (lang && highlighter.getLoadedLanguages().includes(lang)) {
|
|
34
71
|
const inner = highlighter
|
|
35
|
-
.codeToHtml(
|
|
72
|
+
.codeToHtml(code, { lang, theme: THEME })
|
|
36
73
|
.replace(/^<pre[^>]*><code[^>]*>/, '')
|
|
37
74
|
.replace(/<\/code><\/pre>\s*$/, '')
|
|
38
|
-
return `<code class="db-inline-code">${inner}</code>`
|
|
75
|
+
return `<code class="db-inline-code">${keepSpaces(inner)}</code>`
|
|
39
76
|
}
|
|
40
|
-
|
|
77
|
+
// plain inline code: unhighlighted, but its spaces kept too. (Slidev
|
|
78
|
+
// renders markdown asynchronously, so the default rule may return
|
|
79
|
+
// a promise of the HTML.)
|
|
80
|
+
const plain = fallback
|
|
41
81
|
? fallback(tokens, idx, options, env, self)
|
|
42
82
|
: self.renderToken(tokens, idx, options)
|
|
83
|
+
return typeof plain === 'string' ? keepSpaces(plain) : Promise.resolve(plain).then(keepSpaces)
|
|
43
84
|
}
|
|
44
85
|
|
|
45
86
|
// ASCII arrows -> unicode, in plain TEXT only. Code spans/blocks are
|