slidev-theme-tud-db 0.0.7 → 0.0.8
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/package.json +1 -1
- package/setup/shiki.ts +130 -2
- package/styles/db-theme.css +10 -3
- package/vite.config.ts +47 -9
package/package.json
CHANGED
package/setup/shiki.ts
CHANGED
|
@@ -24,7 +24,7 @@ const MARK = /⟦([\w-]+):([^⟧\n]*)⟧/g
|
|
|
24
24
|
|
|
25
25
|
interface Decoration { start: number, end: number, properties: Record<string, string> }
|
|
26
26
|
|
|
27
|
-
const codeMarks = {
|
|
27
|
+
export const codeMarks = {
|
|
28
28
|
name: 'tud-db:code-marks',
|
|
29
29
|
preprocess(code: string, options: { decorations?: Decoration[] }) {
|
|
30
30
|
if (!code.includes('⟦'))
|
|
@@ -46,10 +46,138 @@ const codeMarks = {
|
|
|
46
46
|
},
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
const SQL_FUNCS = /^(COUNT|AVG|SUM|MIN|MAX|COALESCE|ROUND|ABS|CEIL|FLOOR|LENGTH|SUBSTRING|TRIM|UPPER|LOWER|CONCAT|NOW|DATE)$/i
|
|
50
|
+
const SQL_CONSTANTS = /^(TRUE|FALSE|UNKNOWN|NULL)$/i
|
|
51
|
+
const SQL_KEYWORDS = /^(EXPLAIN|ANALYZE)$/i
|
|
52
|
+
const SQL_ALL = /^(TRUE|FALSE|UNKNOWN|NULL|EXPLAIN|ANALYZE|COUNT|AVG|SUM|MIN|MAX|COALESCE|ROUND|ABS|CEIL|FLOOR|LENGTH|SUBSTRING|TRIM|UPPER|LOWER|CONCAT|NOW|DATE)$/i
|
|
53
|
+
const SQL_SPLIT = /\b(TRUE|FALSE|UNKNOWN|NULL|EXPLAIN|ANALYZE|COUNT|AVG|SUM|MIN|MAX|COALESCE|ROUND|ABS|CEIL|FLOOR|LENGTH|SUBSTRING|TRIM|UPPER|LOWER|CONCAT|NOW|DATE)\b/i
|
|
54
|
+
|
|
55
|
+
function getSqlStyle(w: string) {
|
|
56
|
+
if (SQL_FUNCS.test(w)) return { color: '#1E66F5', light: '#1E66F5', dark: '#89B4FA' }
|
|
57
|
+
if (SQL_CONSTANTS.test(w)) return { color: '#FE640B', light: '#FE640B', dark: '#FAB387' }
|
|
58
|
+
return { color: '#8839EF', light: '#8839EF', dark: '#CBA6F7' }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const sqlKeywords = {
|
|
62
|
+
name: 'tud-db:sql-keywords',
|
|
63
|
+
tokens(lines: any[]) {
|
|
64
|
+
if ((this as any).options?.lang !== 'sql') return
|
|
65
|
+
for (let l = 0; l < lines.length; l++) {
|
|
66
|
+
const line = lines[l]
|
|
67
|
+
const newTokens: any[] = []
|
|
68
|
+
for (const token of line) {
|
|
69
|
+
const trimmed = token.content.trim()
|
|
70
|
+
const upper = trimmed.toUpperCase()
|
|
71
|
+
|
|
72
|
+
// 0. Intercept numeric tokens -> light emerald green (#059669)
|
|
73
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
74
|
+
const leading = token.content.slice(0, token.content.indexOf(trimmed))
|
|
75
|
+
const trailing = token.content.slice(token.content.indexOf(trimmed) + trimmed.length)
|
|
76
|
+
if (leading) newTokens.push({ ...token, content: leading, fontStyle: 0, htmlStyle: { color: '#4C4F69', '--shiki-light': '#4C4F69', '--shiki-dark': '#CDD6F4' } })
|
|
77
|
+
newTokens.push({
|
|
78
|
+
...token,
|
|
79
|
+
content: trimmed,
|
|
80
|
+
color: '#059669',
|
|
81
|
+
fontStyle: 0,
|
|
82
|
+
htmlStyle: { color: '#059669', '--shiki-light': '#059669', '--shiki-dark': '#34D399' },
|
|
83
|
+
})
|
|
84
|
+
if (trailing) newTokens.push({ ...token, content: trailing, fontStyle: 0, htmlStyle: { color: '#4C4F69', '--shiki-light': '#4C4F69', '--shiki-dark': '#CDD6F4' } })
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 1. Intercept NULL if Shiki tagged it as keyword purple (including combined tokens like "IS NOT NULL")
|
|
89
|
+
if (/\bNULL\b/i.test(token.content)) {
|
|
90
|
+
const s = getSqlStyle('NULL')
|
|
91
|
+
const parts = token.content.split(/\b(NULL)\b/i)
|
|
92
|
+
let currOffset = token.offset
|
|
93
|
+
for (const part of parts) {
|
|
94
|
+
if (!part) continue
|
|
95
|
+
if (/^NULL$/i.test(part)) {
|
|
96
|
+
newTokens.push({
|
|
97
|
+
...token,
|
|
98
|
+
content: part,
|
|
99
|
+
offset: currOffset,
|
|
100
|
+
color: s.color,
|
|
101
|
+
fontStyle: 0,
|
|
102
|
+
htmlStyle: { color: s.color, '--shiki-light': s.light, '--shiki-dark': s.dark },
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
newTokens.push({ ...token, content: part, offset: currOffset })
|
|
107
|
+
}
|
|
108
|
+
currOffset += part.length
|
|
109
|
+
}
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. Intercept function tokens (AVG, COUNT, COALESCE, etc.) -> upright blue
|
|
114
|
+
// and normalize misclassified identifiers (e.g. `movies` in `CREATE TABLE movies`) -> plain text upright
|
|
115
|
+
const hasItalic = token.fontStyle === 1
|
|
116
|
+
|| token.htmlStyle?.['--shiki-light-font-style'] === 'italic'
|
|
117
|
+
|| token.htmlStyle?.['--shiki-dark-font-style'] === 'italic'
|
|
118
|
+
const isBlue = (token.color?.toLowerCase() === '#1e66f5' || token.htmlStyle?.['--shiki-light']?.toLowerCase() === '#1e66f5')
|
|
119
|
+
if (hasItalic && isBlue) {
|
|
120
|
+
const s = SQL_ALL.test(trimmed)
|
|
121
|
+
? getSqlStyle(trimmed)
|
|
122
|
+
: { color: '#4C4F69', light: '#4C4F69', dark: '#CDD6F4' }
|
|
123
|
+
const leading = token.content.slice(0, token.content.indexOf(trimmed))
|
|
124
|
+
const trailing = token.content.slice(token.content.indexOf(trimmed) + trimmed.length)
|
|
125
|
+
if (leading) newTokens.push({ ...token, content: leading, fontStyle: 0, htmlStyle: { color: '#4C4F69', '--shiki-light': '#4C4F69', '--shiki-dark': '#CDD6F4' } })
|
|
126
|
+
newTokens.push({
|
|
127
|
+
...token,
|
|
128
|
+
content: trimmed,
|
|
129
|
+
color: s.color,
|
|
130
|
+
fontStyle: 0,
|
|
131
|
+
htmlStyle: { color: s.color, '--shiki-light': s.light, '--shiki-dark': s.dark },
|
|
132
|
+
})
|
|
133
|
+
if (trailing) newTokens.push({ ...token, content: trailing, fontStyle: 0, htmlStyle: { color: '#4C4F69', '--shiki-light': '#4C4F69', '--shiki-dark': '#CDD6F4' } })
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 3. Intercept plain text tokens containing SQL words (TRUE, FALSE, UNKNOWN, EXPLAIN, ANALYZE, functions, etc.)
|
|
138
|
+
const lightColor = token.htmlStyle?.['--shiki-light'] || token.htmlStyle?.color || token.color
|
|
139
|
+
const isPlain = !lightColor || lightColor.toLowerCase() === '#4c4f69'
|
|
140
|
+
if (isPlain && SQL_SPLIT.test(token.content)) {
|
|
141
|
+
const parts = token.content.split(SQL_SPLIT)
|
|
142
|
+
let currOffset = token.offset
|
|
143
|
+
for (const part of parts) {
|
|
144
|
+
if (!part) continue
|
|
145
|
+
if (SQL_ALL.test(part)) {
|
|
146
|
+
const s = getSqlStyle(part)
|
|
147
|
+
newTokens.push({
|
|
148
|
+
...token,
|
|
149
|
+
content: part,
|
|
150
|
+
offset: currOffset,
|
|
151
|
+
color: s.color,
|
|
152
|
+
fontStyle: 0,
|
|
153
|
+
htmlStyle: { color: s.color, '--shiki-light': s.light, '--shiki-dark': s.dark },
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
newTokens.push({
|
|
158
|
+
...token,
|
|
159
|
+
content: part,
|
|
160
|
+
offset: currOffset,
|
|
161
|
+
fontStyle: 0,
|
|
162
|
+
htmlStyle: { color: '#4C4F69', '--shiki-light': '#4C4F69', '--shiki-dark': '#CDD6F4' },
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
currOffset += part.length
|
|
166
|
+
}
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
newTokens.push(token)
|
|
171
|
+
}
|
|
172
|
+
lines[l] = newTokens
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
|
|
49
177
|
export default () => ({
|
|
50
178
|
themes: {
|
|
51
179
|
light: 'catppuccin-latte',
|
|
52
180
|
dark: 'catppuccin-mocha',
|
|
53
181
|
},
|
|
54
|
-
transformers: [codeMarks],
|
|
182
|
+
transformers: [codeMarks, sqlKeywords],
|
|
55
183
|
})
|
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.
|
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 { sqlKeywords } 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.
|
|
@@ -9,15 +10,22 @@ import { setupTableCaptions } from './markdown'
|
|
|
9
10
|
// The leading `{lang}` token selects the language; everything after the first
|
|
10
11
|
// space is highlighted with Shiki and rendered inside a normal inline <code>.
|
|
11
12
|
//
|
|
13
|
+
// Common SQL keywords/constants/functions (e.g. `NULL`, `TRUE`, `COUNT`) are
|
|
14
|
+
// automatically detected and highlighted with the theme's SQL grammar.
|
|
15
|
+
//
|
|
12
16
|
// A deck can still override THEME, LANGS, or the rule by defining its own
|
|
13
17
|
// vite.config.ts (the deck's config merges last and wins).
|
|
14
|
-
//
|
|
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
18
|
const THEME = 'catppuccin-latte'
|
|
19
19
|
const LANGS = ['json', 'yaml', 'sql', 'c', 'cpp', 'python']
|
|
20
20
|
|
|
21
|
+
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/i
|
|
22
|
+
const AUTO_SQL_OPS = /^(?:<=>|<>|<=|>=|!=|=|<|>)$/
|
|
23
|
+
|
|
24
|
+
function isAutoSql(code: string): boolean {
|
|
25
|
+
const trimmed = code.trim()
|
|
26
|
+
return AUTO_SQL_OPS.test(trimmed) || AUTO_SQL.test(trimmed)
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
export default async () => {
|
|
22
30
|
const highlighter = await createHighlighter({ themes: [THEME], langs: LANGS })
|
|
23
31
|
|
|
@@ -29,12 +37,42 @@ export default async () => {
|
|
|
29
37
|
setupTableCaptions(md)
|
|
30
38
|
const fallback = md.renderer.rules.code_inline
|
|
31
39
|
md.renderer.rules.code_inline = (tokens: any[], idx: number, options: any, env: any, self: any) => {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
const raw = String(tokens[idx].content)
|
|
41
|
+
const m = raw.match(/^\{([\w-]+)\}\s+([\s\S]+)$/)
|
|
42
|
+
let lang = m ? m[1] : null
|
|
43
|
+
let code = m ? m[2] : raw
|
|
44
|
+
|
|
45
|
+
// Auto-detect common SQL keywords/constants when untagged
|
|
46
|
+
if (!lang && isAutoSql(code)) {
|
|
47
|
+
lang = 'sql'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Auto-detect function calls like `next()` or `child.next()` when untagged -> blue upright
|
|
51
|
+
const fnMatch = !lang && code.trim().match(/^((?:[a-zA-Z_]\w*\.)*)([a-zA-Z_]\w*)\s*(\(.*?\))$/)
|
|
52
|
+
if (fnMatch) {
|
|
53
|
+
const prefix = fnMatch[1]
|
|
54
|
+
const fnName = fnMatch[2]
|
|
55
|
+
const rest = fnMatch[3].replace(/ /g, ' ')
|
|
56
|
+
let inner = ''
|
|
57
|
+
if (prefix) {
|
|
58
|
+
inner += `<span style="color:#4C4F69;--shiki-light:#4C4F69;--shiki-dark:#CDD6F4">${prefix}</span>`
|
|
59
|
+
}
|
|
60
|
+
inner += `<span style="color:#1E66F5;--shiki-light:#1E66F5;--shiki-dark:#89B4FA">${fnName}</span>`
|
|
61
|
+
inner += `<span style="color:#7C7F93;--shiki-light:#7C7F93;--shiki-dark:#9399B2">${rest}</span>`
|
|
62
|
+
return `<code class="db-inline-code">${inner}</code>`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (lang && highlighter.getLoadedLanguages().includes(lang)) {
|
|
66
|
+
let inner = highlighter
|
|
67
|
+
.codeToHtml(code, { lang, theme: THEME, transformers: [sqlKeywords] })
|
|
36
68
|
.replace(/^<pre[^>]*><code[^>]*>/, '')
|
|
37
69
|
.replace(/<\/code><\/pre>\s*$/, '')
|
|
70
|
+
|
|
71
|
+
// Preserve all spaces across tokens so Vue's template compiler never condenses/strips them
|
|
72
|
+
inner = inner.replace(/<span([^>]*)>([^<]*)<\/span>/g, (_m, attrs, text) => {
|
|
73
|
+
return `<span${attrs}>${text.replace(/ /g, ' ')}</span>`
|
|
74
|
+
})
|
|
75
|
+
|
|
38
76
|
return `<code class="db-inline-code">${inner}</code>`
|
|
39
77
|
}
|
|
40
78
|
return fallback
|