@hajimism/dek 0.5.0 → 0.6.0
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/CHANGELOG.md +37 -0
- package/package.json +12 -5
- package/src/cli/address.ts +48 -11
- package/src/cli/build.ts +3 -1
- package/src/cli/commands.ts +24 -10
- package/src/cli/contract.ts +28 -2
- package/src/cli/flags.ts +5 -0
- package/src/cli/keep-alive.ts +12 -6
- package/src/cli/lint.ts +53 -1
- package/src/cli/main.ts +2 -2
- package/src/cli/sync.ts +21 -6
- package/src/cli/text.ts +20 -2
- package/src/cli/theme.ts +1 -1
- package/src/core/build.ts +4 -0
- package/src/core/config-keys.ts +7 -2
- package/src/core/config.ts +63 -0
- package/src/core/diagnostic.ts +9 -1
- package/src/core/document.ts +28 -3
- package/src/core/lint/context.ts +2 -0
- package/src/core/lint/shared-styles.ts +26 -1
- package/src/core/lint/theme-css.ts +64 -3
- package/src/core/lint.ts +15 -2
- package/src/core/page.ts +1 -0
- package/src/core/plugin-host.ts +669 -0
- package/src/core/plugin.ts +436 -0
- package/src/core/sandbox.ts +1 -1
- package/src/core/sarif.ts +47 -5
- package/src/core/slide-measure.ts +1 -0
- package/src/core/spawn.ts +9 -7
- package/src/core/sync.ts +3 -1
- package/src/plugin-api.ts +126 -0
- package/src/plugins/study/check.ts +128 -0
- package/src/plugins/study/main.ts +47 -0
- package/src/plugins/study/manifest.ts +32 -0
- package/src/plugins/study/page.ts +295 -0
- package/src/plugins/study/place.ts +88 -0
- package/src/plugins/study/sarif.ts +30 -0
- package/src/plugins/study/study.ts +137 -0
- package/src/plugins/study/text.ts +55 -0
- package/src/plugins/study/yaml.ts +226 -0
- package/src/plugins/words/check.ts +109 -0
- package/src/plugins/words/lines.ts +26 -0
- package/src/plugins/words/list.ts +288 -0
- package/src/plugins/words/main.ts +50 -0
- package/src/plugins/words/manifest.ts +36 -0
- package/src/plugins/words/match.ts +60 -0
- package/src/plugins/words/sarif.ts +37 -0
- package/src/plugins/words/text.ts +352 -0
- package/src/plugins/words/yaml.ts +225 -0
- package/src/runtime/player.ts +18 -2
- package/src/runtime/plugins.ts +236 -0
- package/src/runtime/start.ts +16 -1
- package/src/runtime/window.ts +3 -0
- package/src/server/deck-snapshot.ts +32 -1
- package/src/server/dev.ts +9 -3
- package/src/server/lock.ts +39 -9
- package/src/server/pages.ts +9 -1
- package/tsconfig.json +1 -1
package/src/core/config.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { DekError } from "./error.ts";
|
|
|
4
4
|
import { shellQuote } from "./escape.ts";
|
|
5
5
|
import { isPackLicense, PACK_LICENSES, type PackLicense } from "./licenses.ts";
|
|
6
6
|
import { parsePublicUrl } from "./ogp.ts";
|
|
7
|
+
import { pluginNameProblem } from "./plugin.ts";
|
|
7
8
|
import {
|
|
8
9
|
isPinFor,
|
|
9
10
|
isPlainRefName,
|
|
@@ -31,6 +32,12 @@ export const Pack = z.object({
|
|
|
31
32
|
.optional(),
|
|
32
33
|
});
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* A plugin's table in `[plugins]`. It takes no key yet: a project chooses which of the plugins the
|
|
37
|
+
* user installed it uses, and never names a path, an argument, or a program for dek to run.
|
|
38
|
+
*/
|
|
39
|
+
export const PluginTable = z.object({});
|
|
40
|
+
|
|
34
41
|
export const DekToml = z.object({
|
|
35
42
|
url: z
|
|
36
43
|
.string()
|
|
@@ -71,6 +78,18 @@ export const DekToml = z.object({
|
|
|
71
78
|
})
|
|
72
79
|
.transform((refs) => refs as Record<string, string>)
|
|
73
80
|
.optional(),
|
|
81
|
+
// A name that is no plugin's is refused without being repeated, as a ref's is.
|
|
82
|
+
plugins: z
|
|
83
|
+
.record(z.string(), PluginTable)
|
|
84
|
+
.superRefine((plugins, ctx) => {
|
|
85
|
+
for (const name of Object.keys(plugins)) {
|
|
86
|
+
const problem = pluginNameProblem(name);
|
|
87
|
+
if (problem) {
|
|
88
|
+
ctx.addIssue({ code: "custom", path: [], message: problem });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
.optional(),
|
|
74
93
|
});
|
|
75
94
|
|
|
76
95
|
const REFUSED_REF_KEY = "a key in [refs] names no ref";
|
|
@@ -107,6 +126,37 @@ function refusedRefKey(source: string): { line?: number } | undefined {
|
|
|
107
126
|
return entry === undefined ? {} : { line: entry.line };
|
|
108
127
|
}
|
|
109
128
|
|
|
129
|
+
/**
|
|
130
|
+
* The first name in [plugins] that is no plugin's, with what is wrong and its line. Like a ref's,
|
|
131
|
+
* the name may be someone else's text, so the error points at it and never repeats it.
|
|
132
|
+
*/
|
|
133
|
+
function refusedPluginKey(source: string): { problem: string; line?: number } | undefined {
|
|
134
|
+
let parsed: unknown;
|
|
135
|
+
try {
|
|
136
|
+
parsed = Bun.TOML.parse(source);
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const plugins = (parsed as { plugins?: unknown } | null)?.plugins;
|
|
141
|
+
if (plugins === null || typeof plugins !== "object") {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
for (const name of Object.keys(plugins)) {
|
|
145
|
+
const problem = pluginNameProblem(name);
|
|
146
|
+
if (problem === undefined) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const written = scanTomlKeys(source).filter(
|
|
150
|
+
(entry) => entry.path[0] === "plugins" && entry.path.length === 2,
|
|
151
|
+
);
|
|
152
|
+
const entry =
|
|
153
|
+
written.find((key) => key.path[1] === name) ??
|
|
154
|
+
written.find((key) => pluginNameProblem(key.path[1] ?? "") !== undefined);
|
|
155
|
+
return entry === undefined ? { problem } : { problem, line: entry.line };
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
110
160
|
type VoiceDefaults = {
|
|
111
161
|
engine: string;
|
|
112
162
|
speaker: string;
|
|
@@ -126,6 +176,8 @@ export type DekConfig = {
|
|
|
126
176
|
pack?: { license?: PackLicense };
|
|
127
177
|
/** `[refs]`: each ref name and what it is pinned to: a commit, or a pack's digest. */
|
|
128
178
|
refs?: Record<string, string>;
|
|
179
|
+
/** `[plugins]`: the name of each plugin the project uses, sorted, as dek runs them. */
|
|
180
|
+
plugins?: string[];
|
|
129
181
|
};
|
|
130
182
|
|
|
131
183
|
export function parseDekToml(source: string, path?: string): DekConfig {
|
|
@@ -133,6 +185,14 @@ export function parseDekToml(source: string, path?: string): DekConfig {
|
|
|
133
185
|
if (refused) {
|
|
134
186
|
throw new DekError(REFUSED_REF_KEY, { path, ...refused, hint: REFUSED_REF_HINT });
|
|
135
187
|
}
|
|
188
|
+
const plugin = refusedPluginKey(source);
|
|
189
|
+
if (plugin) {
|
|
190
|
+
throw new DekError(`a name in [plugins] is no plugin's: ${plugin.problem}`, {
|
|
191
|
+
path,
|
|
192
|
+
...(plugin.line === undefined ? {} : { line: plugin.line }),
|
|
193
|
+
hint: "rename the table to the <name> of the dek-<name> you installed, or delete it",
|
|
194
|
+
});
|
|
195
|
+
}
|
|
136
196
|
const data = parseTomlWith(DekToml, source, {
|
|
137
197
|
label: "dek.toml",
|
|
138
198
|
anchor: "dek-toml",
|
|
@@ -155,6 +215,9 @@ export function parseDekToml(source: string, path?: string): DekConfig {
|
|
|
155
215
|
? { pack: data.pack.license ? { license: data.pack.license as PackLicense } : {} }
|
|
156
216
|
: {}),
|
|
157
217
|
...(data.refs && Object.keys(data.refs).length > 0 ? { refs: data.refs } : {}),
|
|
218
|
+
...(data.plugins && Object.keys(data.plugins).length > 0
|
|
219
|
+
? { plugins: Object.keys(data.plugins).sort() }
|
|
220
|
+
: {}),
|
|
158
221
|
};
|
|
159
222
|
}
|
|
160
223
|
|
package/src/core/diagnostic.ts
CHANGED
|
@@ -64,6 +64,7 @@ export const RULES = {
|
|
|
64
64
|
DEK043: { severity: "warning", scopes: ["deck"] },
|
|
65
65
|
DEK044: { severity: "warning", scopes: ["deck", "slide"] },
|
|
66
66
|
DEK045: { severity: "warning", scopes: ["deck"] },
|
|
67
|
+
DEK046: { severity: "error", scopes: ["deck"] },
|
|
67
68
|
} as const satisfies Record<string, { severity: Severity; scopes: readonly Scope[] }>;
|
|
68
69
|
|
|
69
70
|
export type RuleId = keyof typeof RULES;
|
|
@@ -113,7 +114,14 @@ export function hasErrors(diagnostics: Diagnostic[]): boolean {
|
|
|
113
114
|
*/
|
|
114
115
|
export type SkippedCheck = {
|
|
115
116
|
/** `preview` is a build's link preview image, which needs a URL and Playwright. */
|
|
116
|
-
check: "lint" | "preview" | "rumdl" | "visual" | "voice";
|
|
117
|
+
check: "lint" | "plugin" | "preview" | "rumdl" | "visual" | "voice";
|
|
118
|
+
/** The plugin, for a `plugin` check: its name in dek.toml `[plugins]`. */
|
|
119
|
+
plugin?: string;
|
|
117
120
|
reason: string;
|
|
118
121
|
hint?: string;
|
|
119
122
|
};
|
|
123
|
+
|
|
124
|
+
/** A skipped check as text names it: the check, or the plugin for a plugin's. */
|
|
125
|
+
export function skippedLabel(entry: SkippedCheck): string {
|
|
126
|
+
return entry.plugin === undefined ? entry.check : `plugin ${entry.plugin}`;
|
|
127
|
+
}
|
package/src/core/document.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { escapeAttr, escapeHtml } from "./escape.ts";
|
|
|
4
4
|
import { collectSlidesHtml, htmlShell } from "./html.ts";
|
|
5
5
|
import { type OgpImage, ogpHead } from "./ogp.ts";
|
|
6
6
|
import { PAGE_ID, type PageMode, pageConfigAttrs } from "./page.ts";
|
|
7
|
+
import type { PluginPage } from "./plugin.ts";
|
|
7
8
|
import {
|
|
8
9
|
nextPresenterTitle,
|
|
9
10
|
presenterSlides,
|
|
@@ -49,14 +50,19 @@ export type PageTarget =
|
|
|
49
50
|
/** The deck at `dir`, rendered for `target`. */
|
|
50
51
|
export function renderDeckHtml(
|
|
51
52
|
dir: string,
|
|
52
|
-
options: { playerScript: string; target: PageTarget },
|
|
53
|
+
options: { playerScript: string; target: PageTarget; plugins?: PluginPage[] },
|
|
53
54
|
): string {
|
|
54
55
|
return renderDeckDocument(resolveDeck(dir).deck, options);
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export function renderDeckDocument(
|
|
58
59
|
deck: ProjectDeck,
|
|
59
|
-
options: {
|
|
60
|
+
options: {
|
|
61
|
+
playerScript: string;
|
|
62
|
+
target: PageTarget;
|
|
63
|
+
/** Each plugin's page; only the player and the build carry them (ADR 33). */
|
|
64
|
+
plugins?: PluginPage[];
|
|
65
|
+
},
|
|
60
66
|
): string {
|
|
61
67
|
const { target } = options;
|
|
62
68
|
const mode: PageMode =
|
|
@@ -160,7 +166,8 @@ export function renderDeckDocument(
|
|
|
160
166
|
${slideScriptTags(slideScripts)}
|
|
161
167
|
<script>${options.playerScript}</script>
|
|
162
168
|
${target.kind === "dev" ? `<script>${target.liveReloadScript}</script>` : ""}
|
|
163
|
-
${target.kind === "dev" && includeNotes ? `<script>${target.annotateScript}</script>` : ""}
|
|
169
|
+
${target.kind === "dev" && includeNotes ? `<script>${target.annotateScript}</script>` : ""}
|
|
170
|
+
${pluginPagesHtml(target, options.plugins ?? [])}`,
|
|
164
171
|
});
|
|
165
172
|
}
|
|
166
173
|
|
|
@@ -225,6 +232,24 @@ function renderKeyHintHtml(lang: string): string {
|
|
|
225
232
|
return `<div id="${PAGE_ID.hint}" role="status"><span><kbd>s</kbd>${labels.rail}</span><span><kbd>p</kbd>${labels.presenter}</span></div>`;
|
|
226
233
|
}
|
|
227
234
|
|
|
235
|
+
/**
|
|
236
|
+
* The plugins' pages, after the player, which gives them `dek.plugin`: their keys and data as one
|
|
237
|
+
* JSON block, then each script in name order. Only the player and the build carry them; the
|
|
238
|
+
* presenter view and a video, like every still page, never do.
|
|
239
|
+
*/
|
|
240
|
+
function pluginPagesHtml(target: PageTarget, plugins: PluginPage[]): string {
|
|
241
|
+
const carried = target.kind === "build" || (target.kind === "dev" && target.mode === "player");
|
|
242
|
+
if (!carried || plugins.length === 0) {
|
|
243
|
+
return "";
|
|
244
|
+
}
|
|
245
|
+
const ordered = plugins.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
246
|
+
const config = Object.fromEntries(ordered.map(({ name, keys, data }) => [name, { keys, data }]));
|
|
247
|
+
return [
|
|
248
|
+
`<script type="application/json" id="${PAGE_ID.plugins}">${jsonForScript(config)}</script>`,
|
|
249
|
+
...ordered.map(({ script }) => `<script>${script.replace(/<\/(script)/gi, "<\\/$1")}</script>`),
|
|
250
|
+
].join("\n ");
|
|
251
|
+
}
|
|
252
|
+
|
|
228
253
|
function jsonForScript(value: unknown): string {
|
|
229
254
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
230
255
|
}
|
package/src/core/lint/context.ts
CHANGED
|
@@ -26,6 +26,8 @@ type SlideSource = { html: string; scan: HtmlScan; skeleton: boolean };
|
|
|
26
26
|
export type LintContext = DeckFiles & {
|
|
27
27
|
/** DEK016 and DEK017 of each slide script, by slug: evaluated before the rules run. */
|
|
28
28
|
scripts: Map<string, Diagnostic[]>;
|
|
29
|
+
/** The source of each slide script lint evaluated, by slug. */
|
|
30
|
+
scriptCode: Map<string, string>;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
/** The deck as read from disk, each file once. */
|
|
@@ -2,6 +2,7 @@ import { isKeyframesPrelude, selectorClasses, splitSelectorList, startsAtSlide }
|
|
|
2
2
|
import { deckPaths } from "../deck-paths.ts";
|
|
3
3
|
import { type Diagnostic, diag } from "../diagnostic.ts";
|
|
4
4
|
import { listed } from "../prose.ts";
|
|
5
|
+
import { RUNTIME_CLASSES } from "../theme-facts.ts";
|
|
5
6
|
import type { LintContext } from "./context.ts";
|
|
6
7
|
|
|
7
8
|
/** How many slides may style something alike before it belongs in the theme: the third copy. */
|
|
@@ -56,6 +57,21 @@ function alsoReaches(ctx: LintContext, selector: string, setters: string[]): str
|
|
|
56
57
|
});
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
/**
|
|
61
|
+
* How many classes theme.css would define with a rule under `selector` moved into it, when that
|
|
62
|
+
* is more than the deck's `max_classes` (DEK013); nothing while the move stays within it.
|
|
63
|
+
*/
|
|
64
|
+
function overBudget(ctx: LintContext, selector: string): number | undefined {
|
|
65
|
+
const defined = ctx.theme?.classes ?? new Set<string>();
|
|
66
|
+
const added = new Set(
|
|
67
|
+
selectorClasses(themeSelector(selector)).filter(
|
|
68
|
+
(name) => !defined.has(name) && !RUNTIME_CLASSES.has(name),
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
const after = defined.size + added.size;
|
|
72
|
+
return added.size > 0 && after > ctx.deck.deck.max_classes ? after : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
/**
|
|
60
76
|
* DEK026: the same declaration under the same selector in the stylesheets of SHARED_BY slides
|
|
61
77
|
* or more. Each slide fixed alike on its own, as agents working on one slide each will, is one
|
|
@@ -146,18 +162,27 @@ export function sharedStyleDiagnostics(ctx: LintContext): Diagnostic[] {
|
|
|
146
162
|
.join(
|
|
147
163
|
"",
|
|
148
164
|
)} without it, so check ${reaches.length === 1 ? "that slide" : "those slides"} after the move`;
|
|
165
|
+
// Following the hint must not trade this warning for DEK013's error.
|
|
166
|
+
const themeClasses = overBudget(ctx, selector);
|
|
167
|
+
const budget =
|
|
168
|
+
themeClasses === undefined
|
|
169
|
+
? ""
|
|
170
|
+
: `; theme.css would then define ${themeClasses} classes, past max_classes ${ctx.deck.deck.max_classes}, so set \`max_classes: ${themeClasses}\` in the frontmatter too, or leave the rule in the slides`;
|
|
149
171
|
return diag("DEK026", {
|
|
150
172
|
message: `${rule} is also in ${listed(others)}`,
|
|
151
173
|
path: paths.slide(slug, ".css"),
|
|
152
174
|
line,
|
|
153
175
|
slug,
|
|
154
|
-
hint: `write ${themeRule} once in theme.css and delete it from ${listed(slugs.map(cssOf))}${affected}`,
|
|
176
|
+
hint: `write ${themeRule} once in theme.css and delete it from ${listed(slugs.map(cssOf))}${affected}${budget}`,
|
|
155
177
|
data: {
|
|
156
178
|
selector,
|
|
157
179
|
declarations,
|
|
158
180
|
slides: slugs,
|
|
159
181
|
...(atPath.length > 0 ? { atRules: atPath } : {}),
|
|
160
182
|
...(reaches.length > 0 ? { alsoReaches: reaches } : {}),
|
|
183
|
+
...(themeClasses !== undefined
|
|
184
|
+
? { themeClasses, limit: ctx.deck.deck.max_classes }
|
|
185
|
+
: {}),
|
|
161
186
|
},
|
|
162
187
|
});
|
|
163
188
|
}),
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
} from "../css.ts";
|
|
13
13
|
import { deckPaths } from "../deck-paths.ts";
|
|
14
14
|
import { type Diagnostic, diag } from "../diagnostic.ts";
|
|
15
|
+
import { escapeRegExp } from "../escape.ts";
|
|
16
|
+
import { listed } from "../prose.ts";
|
|
15
17
|
import { assetRefDiagnostics } from "./asset-refs.ts";
|
|
16
18
|
import type { LintContext } from "./context.ts";
|
|
17
19
|
import { isRawThemeValue, isRawTokenValue, REQUIRED_TOKENS, tokenSuggestion } from "./tokens.ts";
|
|
@@ -24,14 +26,18 @@ function missingThemeDiagnostic(ctx: LintContext): Diagnostic {
|
|
|
24
26
|
return diag("DEK018", {
|
|
25
27
|
message: "theme.css not found",
|
|
26
28
|
path: deckPaths(ctx.deck.dir).theme,
|
|
27
|
-
hint: "copy
|
|
29
|
+
hint: "copy the project's template, ../../theme.css, or another deck's theme into the deck directory",
|
|
28
30
|
});
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/** DEK012, DEK013, DEK015, DEK014, and what its `url()`s load (DEK020 to DEK023) for theme.css. */
|
|
32
34
|
function lintTheme(
|
|
33
35
|
{ path, sheet, classes, tokens }: NonNullable<LintContext["theme"]>,
|
|
34
|
-
{
|
|
36
|
+
{
|
|
37
|
+
maxClasses,
|
|
38
|
+
deckDir,
|
|
39
|
+
usedBy,
|
|
40
|
+
}: { maxClasses: number; deckDir: string; usedBy: Map<string, string[]> | undefined },
|
|
35
41
|
): Diagnostic[] {
|
|
36
42
|
const diagnostics: Diagnostic[] = [];
|
|
37
43
|
// A rule inside `@media` reaches the page as surely as one outside it.
|
|
@@ -56,6 +62,7 @@ function lintTheme(
|
|
|
56
62
|
diag("DEK013", {
|
|
57
63
|
message: `theme.css has ${classCount} classes; limit is ${maxClasses}`,
|
|
58
64
|
path,
|
|
65
|
+
hint: classBudgetHint(classCount, usedBy),
|
|
59
66
|
data: { classes: classCount, limit: maxClasses },
|
|
60
67
|
}),
|
|
61
68
|
);
|
|
@@ -287,9 +294,63 @@ function themeScopeHint(selector: string): string {
|
|
|
287
294
|
return `write it as ${scoped.join(", ")}`;
|
|
288
295
|
}
|
|
289
296
|
|
|
297
|
+
/**
|
|
298
|
+
* How to bring the theme back within its class budget: first what frees a class without changing
|
|
299
|
+
* a slide, a class no slide uses and one only one slide uses, then raising the budget itself.
|
|
300
|
+
*/
|
|
301
|
+
function classBudgetHint(classCount: number, usedBy: Map<string, string[]> | undefined): string {
|
|
302
|
+
if (usedBy === undefined) {
|
|
303
|
+
return `set \`max_classes: ${classCount}\` in the frontmatter`;
|
|
304
|
+
}
|
|
305
|
+
const unused = [...usedBy].filter(([, slugs]) => slugs.length === 0).map(([name]) => `.${name}`);
|
|
306
|
+
const single = [...usedBy].flatMap(([name, slugs]) =>
|
|
307
|
+
slugs.length === 1 ? [`.${name} into slides/${slugs[0]}.css`] : [],
|
|
308
|
+
);
|
|
309
|
+
return [
|
|
310
|
+
...(unused.length > 0 ? [`delete ${listed(unused)}, which no slide uses`] : []),
|
|
311
|
+
...(single.length > 0
|
|
312
|
+
? [`move each class only one slide uses into that slide's stylesheet: ${listed(single)}`]
|
|
313
|
+
: []),
|
|
314
|
+
`set \`max_classes: ${classCount}\` in the frontmatter`,
|
|
315
|
+
].join("; or ");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Each class the theme defines but `.slide`, which every slide is, to the slides that use it, in
|
|
320
|
+
* script order: in their markup, or anywhere in their script, which may set it with `classList`.
|
|
321
|
+
* None while a section has no HTML yet or only its skeleton: what it will use is unknown.
|
|
322
|
+
*/
|
|
323
|
+
function slidesUsing(ctx: LintContext, classes: Set<string>): Map<string, string[]> | undefined {
|
|
324
|
+
const usedBy = new Map(
|
|
325
|
+
[...classes].filter((name) => name !== "slide").map((name) => [name, [] as string[]]),
|
|
326
|
+
);
|
|
327
|
+
for (const slug of ctx.sectionsBySlug.keys()) {
|
|
328
|
+
const source = ctx.slideSource(slug);
|
|
329
|
+
if (source === undefined || source.skeleton) {
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
const code = ctx.scriptCode.get(slug);
|
|
333
|
+
for (const [name, slugs] of usedBy) {
|
|
334
|
+
if (source.scan.classes.includes(name) || (code !== undefined && namesClass(code, name))) {
|
|
335
|
+
slugs.push(slug);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return usedBy;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Whether `code` holds `name` as a class token: a word of its own, wherever it stands. */
|
|
343
|
+
function namesClass(code: string, name: string): boolean {
|
|
344
|
+
return new RegExp(`(^|[^\\w-])${escapeRegExp(name)}($|[^\\w-])`).test(code);
|
|
345
|
+
}
|
|
346
|
+
|
|
290
347
|
/** The theme's own findings, or DEK018 when there is no theme. */
|
|
291
348
|
export function themeDiagnostics(ctx: LintContext): Diagnostic[] {
|
|
292
349
|
return ctx.theme
|
|
293
|
-
? lintTheme(ctx.theme, {
|
|
350
|
+
? lintTheme(ctx.theme, {
|
|
351
|
+
maxClasses: ctx.deck.deck.max_classes,
|
|
352
|
+
deckDir: ctx.deck.dir,
|
|
353
|
+
usedBy: slidesUsing(ctx, ctx.theme.classes),
|
|
354
|
+
})
|
|
294
355
|
: [missingThemeDiagnostic(ctx)];
|
|
295
356
|
}
|
package/src/core/lint.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { timingDiagnostics } from "./lint/timing.ts";
|
|
|
18
18
|
import { voiceDiagnostics } from "./lint/voice.ts";
|
|
19
19
|
import { asResolvedDeck, type Project, type ProjectDeck } from "./resolve.ts";
|
|
20
20
|
import type { Section } from "./schema.ts";
|
|
21
|
+
import type { SlideScriptProblem } from "./slide-script.ts";
|
|
21
22
|
import { evaluateSlideScripts, slideScriptsProblems } from "./slide-script-eval.ts";
|
|
22
23
|
import { RUNTIME_CLASSES } from "./theme-facts.ts";
|
|
23
24
|
|
|
@@ -53,7 +54,7 @@ export function lintDeck(
|
|
|
53
54
|
export function lintDeck(input: DeckInput, options?: LintDeckOptions): Diagnostic[] {
|
|
54
55
|
const { files, scripted } = readLintInput(input);
|
|
55
56
|
const problems = slideScriptsProblems(scripted.map((entry) => entry.input));
|
|
56
|
-
return runRules(
|
|
57
|
+
return runRules(lintContext(files, scripted, problems), options);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/**
|
|
@@ -66,7 +67,19 @@ export async function lintDeckAsync(
|
|
|
66
67
|
): Promise<Diagnostic[]> {
|
|
67
68
|
const { files, scripted } = readLintInput(input);
|
|
68
69
|
const problems = await evaluateSlideScripts(scripted.map((entry) => entry.input));
|
|
69
|
-
return runRules(
|
|
70
|
+
return runRules(lintContext(files, scripted, problems), options);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function lintContext(
|
|
74
|
+
files: DeckFiles,
|
|
75
|
+
scripted: LintedScript[],
|
|
76
|
+
problems: SlideScriptProblem[][],
|
|
77
|
+
): LintContext {
|
|
78
|
+
return {
|
|
79
|
+
...files,
|
|
80
|
+
scripts: slideScriptDiagnostics(scripted, problems),
|
|
81
|
+
scriptCode: new Map(scripted.map(({ section, input }) => [section.slug, input.code])),
|
|
82
|
+
};
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
function readLintInput(input: DeckInput): { files: DeckFiles; scripted: LintedScript[] } {
|