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 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` (fenced) and the `THEME` in `vite.config.ts` (inline `{lang}`) —
57
- the **two** highlighters; keep them in sync (both `catppuccin-latte`).
58
- `setup/shiki.ts` also holds the transformer behind `⟦name:text⟧` code marks
59
- (spans a slide's timeline can recolour).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidev-theme-tud-db",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "description": "Slidev theme cloning the TU Dresden / Dresden Database Research Group PowerPoint template",
6
6
  "license": "GPL-3.0-or-later",
package/setup/shiki.ts CHANGED
@@ -1,10 +1,12 @@
1
- // Default Shiki color scheme for fenced code blocks (```lang ... ```).
2
- // Matches the inline-code highlighter (see vite.config.ts) and the chair's
3
- // template, so inline and fenced code look the same.
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
- // catppuccin-latte / -mocha are bundled with Shiki, so no extra deps needed.
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
- export default () => ({
50
- themes: {
51
- light: 'catppuccin-latte',
52
- dark: 'catppuccin-mocha',
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
- transformers: [codeMarks],
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
+ }
@@ -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: 500; }
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
- // A deck can still override THEME, LANGS, or the rule by defining its own
13
- // vite.config.ts (the deck's config merges last and wins).
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
- // NB: no `import ... from 'vite'` here on purpose — `defineConfig` is only a
16
- // type helper, and importing it would require `vite` to be resolvable from this
17
- // file's location (it isn't, when the theme is loaded from its own directory).
18
- const THEME = 'catppuccin-latte'
19
- const LANGS = ['json', 'yaml', 'sql', 'c', 'cpp', 'python']
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
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 &nbsp;; 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) => `>${'&nbsp;'.repeat(sp.length)}<`)
38
+ .replace(/ {2,}/g, sp => `${'&nbsp;'.repeat(sp.length - 1)} `)
39
+ }
20
40
 
21
41
  export default async () => {
22
- const highlighter = await createHighlighter({ themes: [THEME], langs: LANGS })
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 m = String(tokens[idx].content).match(/^\{([\w-]+)\}\s+([\s\S]+)$/)
33
- if (m && highlighter.getLoadedLanguages().includes(m[1])) {
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(m[2], { lang: m[1], theme: THEME })
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
- return fallback
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