Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/quarto-core/text-highlighting.ts
12924 views
1
/*
2
* text-highlighting.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
import { join } from "../deno_ral/path.ts";
7
8
import { kDefaultHighlightStyle } from "../command/render/constants.ts";
9
import { kHighlightStyle, kSyntaxHighlighting } from "../config/constants.ts";
10
import { FormatPandoc } from "../config/types.ts";
11
12
import { existsSync } from "../deno_ral/fs.ts";
13
import { resourcePath } from "../core/resources.ts";
14
import { normalizePath } from "../core/path.ts";
15
import { warnOnce } from "../core/log.ts";
16
17
export interface ThemeDescriptor {
18
json: Record<string, unknown>;
19
isAdaptive: boolean;
20
}
21
22
const kDarkSuffix = "dark";
23
const kLightSuffix = "light";
24
25
// Resolve highlight theme from syntax-highlighting (new) or highlight-style (deprecated)
26
export function getHighlightTheme(
27
pandoc: FormatPandoc,
28
): string | Record<string, string> {
29
return pandoc[kSyntaxHighlighting] ||
30
pandoc[kHighlightStyle] ||
31
kDefaultHighlightStyle;
32
}
33
34
export function textHighlightThemePath(
35
inputDir: string,
36
theme: string | Record<string, string>,
37
style?: "dark" | "light",
38
) {
39
let resolvedTheme: string;
40
if (typeof theme === "object") {
41
if (style && theme[style]) {
42
resolvedTheme = theme[style] as string;
43
} else {
44
resolvedTheme = theme[Object.keys(theme)[0]] as string;
45
}
46
} else {
47
resolvedTheme = theme as string;
48
}
49
50
// First try the style specific version of the theme, otherwise
51
// fall back to the plain name
52
const names = [
53
`${resolvedTheme}-${style === "dark" ? kDarkSuffix : kLightSuffix}`,
54
resolvedTheme,
55
];
56
57
const themePath = names.map((name) => {
58
return resourcePath(join("pandoc", "highlight-styles", `${name}.theme`));
59
}).find((path) => existsSync(path));
60
61
if (themePath) {
62
// first see if this matches a built in name
63
return themePath;
64
} else {
65
// see if this is a path to a user theme
66
const userThemePath = join(inputDir, resolvedTheme);
67
if (existsSync(userThemePath)) {
68
return normalizePath(userThemePath);
69
}
70
}
71
72
// Could find a path
73
return undefined;
74
}
75
76
export function readHighlightingTheme(
77
inputDir: string,
78
pandoc: FormatPandoc,
79
style: "dark" | "light" | "default",
80
): ThemeDescriptor | undefined {
81
const theme = getHighlightTheme(pandoc);
82
const themeRaw = readTheme(inputDir, theme, style);
83
if (themeRaw) {
84
return {
85
json: JSON.parse(themeRaw),
86
isAdaptive: isAdaptiveTheme(theme),
87
};
88
}
89
return undefined;
90
}
91
92
export function hasAdaptiveTheme(pandoc: FormatPandoc) {
93
return isAdaptiveTheme(getHighlightTheme(pandoc));
94
}
95
96
export function hasTextHighlighting(pandoc: FormatPandoc): boolean {
97
return getHighlightTheme(pandoc) !== "none";
98
}
99
100
export function isAdaptiveTheme(theme: string | Record<string, string>) {
101
if (typeof theme === "string") {
102
return [
103
"a11y",
104
"arrow",
105
"atom-one",
106
"ayu",
107
"breeze",
108
"github",
109
"gruvbox",
110
"monochrome",
111
].includes(
112
theme,
113
);
114
} else {
115
const keys = Object.keys(theme);
116
return keys.includes("dark") && keys.includes("light");
117
}
118
}
119
120
// Reads the contents of a theme file, falling back if the style specific version isn't available
121
export function readTheme(
122
inputDir: string,
123
theme: string | Record<string, string>,
124
style: "light" | "dark" | "default",
125
) {
126
const themeFile = textHighlightThemePath(
127
inputDir,
128
theme,
129
style === "default" ? undefined : style,
130
);
131
if (!themeFile) {
132
return undefined;
133
}
134
135
if (!existsSync(themeFile)) {
136
warnOnce(`The text highlighting theme ${themeFile} does not exist.`);
137
return undefined;
138
}
139
140
if (Deno.statSync(themeFile).isDirectory) {
141
throw new Error(
142
`The text highlighting theme ${themeFile} is a directory. Please provide a valid theme name or path to a .theme file.`,
143
);
144
}
145
return Deno.readTextFileSync(themeFile);
146
}
147
148
// From https://github.com/jgm/skylighting/blob/a1d02a0db6260c73aaf04aae2e6e18b569caacdc/skylighting-core/src/Skylighting/Format/HTML.hs#L117-L147
149
export const kAbbrevs: Record<string, string> = {
150
"Keyword": "kw",
151
"DataType": "dt",
152
"DecVal": "dv",
153
"BaseN": "bn",
154
"Float": "fl",
155
"Char": "ch",
156
"String": "st",
157
"Comment": "co",
158
"Other": "ot",
159
"Alert": "al",
160
"Function": "fu",
161
"RegionMarker": "re",
162
"Error": "er",
163
"Constant": "cn",
164
"SpecialChar": "sc",
165
"VerbatimString": "vs",
166
"SpecialString": "ss",
167
"Import": "im",
168
"Documentation": "do",
169
"Annotation": "an",
170
"CommentVar": "cv",
171
"Variable": "va",
172
"ControlFlow": "cf",
173
"Operator": "op",
174
"BuiltIn": "bu",
175
"Extension": "ex",
176
"Preprocessor": "pp",
177
"Attribute": "at",
178
"Information": "in",
179
"Warning": "wa",
180
"Normal": "",
181
};
182
183