@openforge-app/plugin-sdk 0.2.9 → 0.2.11
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/dist/backend.d.ts +2 -2
- package/dist/collapsibleSectionState.js +30 -12
- package/dist/domain.d.ts +1 -0
- package/dist/domain.js +2 -1
- package/dist/frontend.d.ts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/manifest.d.ts +1 -0
- package/dist/manifest.js +10 -4
- package/dist/markdown.d.ts +9 -0
- package/dist/markdown.js +57 -1
- package/dist/mermaid.d.ts +13 -0
- package/dist/mermaid.js +221 -0
- package/dist/mermaidZoom.d.ts +23 -0
- package/dist/mermaidZoom.js +52 -0
- package/dist/openforgePackageMetadataSchema.json +2 -11
- package/dist/publicEntrypoints.d.mts +27 -0
- package/dist/publicEntrypoints.mjs +104 -0
- package/dist/publicUiExports.mjs +12 -22
- package/dist/registryValidation.d.mts +11 -0
- package/dist/registryValidation.mjs +30 -0
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.js +131 -0
- package/dist/taskBrowserDevToolsShortcuts.d.ts +12 -0
- package/dist/taskBrowserDevToolsShortcuts.js +20 -0
- package/dist/testing/commonApiFake.js +240 -7
- package/dist/testing/contracts.d.ts +15 -3
- package/dist/testing/frontendContributionFake.d.ts +2 -3
- package/dist/testing/frontendContributionFake.js +0 -32
- package/dist/testing/support.d.ts +5 -2
- package/dist/testing/support.js +12 -1
- package/dist/types.d.ts +91 -20
- package/dist/types.js +1 -1
- package/dist/ui/MarkdownContent.svelte +46 -0
- package/dist/ui/MermaidDiagramPreview.svelte +216 -0
- package/dist/ui/Modal.svelte +39 -4
- package/dist/ui/PluginPageHeader.svelte +11 -4
- package/dist/ui/PluginPageShell.svelte +20 -0
- package/dist/ui/ProjectFileTree.svelte +192 -0
- package/dist/vite.js +7 -17
- package/package.json +14 -6
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type OpenForgePluginSdkConditionalExport = Readonly<{
|
|
2
|
+
types: `./dist/${string}.d.ts`
|
|
3
|
+
default: `./dist/${string}.js`
|
|
4
|
+
}>
|
|
5
|
+
|
|
6
|
+
export type OpenForgePluginSdkPublicEntrypoint = Readonly<{
|
|
7
|
+
packageSubpath: '.' | `./${string}`
|
|
8
|
+
importSpecifier: '@openforge-app/plugin-sdk' | `@openforge-app/plugin-sdk/${string}`
|
|
9
|
+
sourcePath: `src/${string}`
|
|
10
|
+
workspaceSourcePath: `packages/plugin-sdk/src/${string}`
|
|
11
|
+
packageExport: string | OpenForgePluginSdkConditionalExport
|
|
12
|
+
}>
|
|
13
|
+
|
|
14
|
+
export const OPENFORGE_PLUGIN_SDK_PUBLIC_ENTRYPOINTS: readonly OpenForgePluginSdkPublicEntrypoint[]
|
|
15
|
+
|
|
16
|
+
export function createOpenForgePluginSdkPackageExports(): Record<
|
|
17
|
+
string,
|
|
18
|
+
string | { types: string; default: string }
|
|
19
|
+
>
|
|
20
|
+
|
|
21
|
+
export function createOpenForgePluginSdkTypeScriptPaths(): Record<string, [string]>
|
|
22
|
+
|
|
23
|
+
export function loadOpenForgePluginSdkTypeScriptPaths(workspaceRoot: string): Promise<Record<string, unknown>>
|
|
24
|
+
export function assertOpenForgePluginSdkEntrypointRegistries(registries: {
|
|
25
|
+
packageExports: unknown
|
|
26
|
+
typeScriptPaths: unknown
|
|
27
|
+
}): void
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { assertRegistryMatchesCanonicalManifest } from './registryValidation.mjs'
|
|
4
|
+
import { OPENFORGE_PLUGIN_SDK_PUBLIC_UI_EXPORTS } from './publicUiExports.mjs'
|
|
5
|
+
|
|
6
|
+
const PLUGIN_SDK_PACKAGE_NAME = '@openforge-app/plugin-sdk'
|
|
7
|
+
|
|
8
|
+
function moduleEntrypoint(packageSubpath, sourceName) {
|
|
9
|
+
const importSuffix = packageSubpath === '.' ? '' : packageSubpath.slice(1)
|
|
10
|
+
return Object.freeze({
|
|
11
|
+
packageSubpath,
|
|
12
|
+
importSpecifier: `${PLUGIN_SDK_PACKAGE_NAME}${importSuffix}`,
|
|
13
|
+
sourcePath: `src/${sourceName}.ts`,
|
|
14
|
+
workspaceSourcePath: `packages/plugin-sdk/src/${sourceName}.ts`,
|
|
15
|
+
packageExport: Object.freeze({
|
|
16
|
+
types: `./dist/${sourceName}.d.ts`,
|
|
17
|
+
default: `./dist/${sourceName}.js`,
|
|
18
|
+
}),
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PUBLIC_MODULE_ENTRYPOINTS = [
|
|
23
|
+
moduleEntrypoint('.', 'index'),
|
|
24
|
+
moduleEntrypoint('./frontend', 'frontend'),
|
|
25
|
+
moduleEntrypoint('./backend', 'backend'),
|
|
26
|
+
moduleEntrypoint('./testing', 'testing'),
|
|
27
|
+
moduleEntrypoint('./vite', 'vite'),
|
|
28
|
+
moduleEntrypoint('./domain', 'domain'),
|
|
29
|
+
moduleEntrypoint('./prStatusPresentation', 'prStatusPresentation'),
|
|
30
|
+
moduleEntrypoint('./markdown', 'markdown'),
|
|
31
|
+
moduleEntrypoint('./numberParsing', 'numberParsing'),
|
|
32
|
+
moduleEntrypoint('./projectFileTree', 'projectFileTree'),
|
|
33
|
+
moduleEntrypoint('./sanitize', 'sanitize'),
|
|
34
|
+
moduleEntrypoint('./pluginIcons', 'pluginIcons'),
|
|
35
|
+
moduleEntrypoint('./fileIcons', 'fileIcons'),
|
|
36
|
+
moduleEntrypoint('./collapsibleSectionState', 'collapsibleSectionState'),
|
|
37
|
+
moduleEntrypoint('./taskBrowserDevToolsShortcuts', 'taskBrowserDevToolsShortcuts'),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
const PACKAGE_METADATA_SCHEMA_ENTRYPOINT = Object.freeze({
|
|
41
|
+
packageSubpath: './package-metadata-schema.json',
|
|
42
|
+
importSpecifier: `${PLUGIN_SDK_PACKAGE_NAME}/package-metadata-schema.json`,
|
|
43
|
+
sourcePath: 'src/openforgePackageMetadataSchema.json',
|
|
44
|
+
workspaceSourcePath: 'packages/plugin-sdk/src/openforgePackageMetadataSchema.json',
|
|
45
|
+
packageExport: './dist/openforgePackageMetadataSchema.json',
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const PUBLIC_UI_ENTRYPOINTS = OPENFORGE_PLUGIN_SDK_PUBLIC_UI_EXPORTS.map((entrypoint) => Object.freeze({
|
|
49
|
+
packageSubpath: entrypoint.packageSubpath,
|
|
50
|
+
importSpecifier: entrypoint.importSpecifier,
|
|
51
|
+
sourcePath: entrypoint.sourcePath,
|
|
52
|
+
workspaceSourcePath: entrypoint.workspaceSourcePath,
|
|
53
|
+
packageExport: entrypoint.distPath,
|
|
54
|
+
}))
|
|
55
|
+
|
|
56
|
+
/** Canonical registrations for every public Plugin SDK entrypoint. */
|
|
57
|
+
export const OPENFORGE_PLUGIN_SDK_PUBLIC_ENTRYPOINTS = Object.freeze([
|
|
58
|
+
...PUBLIC_MODULE_ENTRYPOINTS.slice(0, 5),
|
|
59
|
+
PACKAGE_METADATA_SCHEMA_ENTRYPOINT,
|
|
60
|
+
...PUBLIC_MODULE_ENTRYPOINTS.slice(5),
|
|
61
|
+
...PUBLIC_UI_ENTRYPOINTS,
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
export function createOpenForgePluginSdkPackageExports() {
|
|
65
|
+
return Object.fromEntries(
|
|
66
|
+
OPENFORGE_PLUGIN_SDK_PUBLIC_ENTRYPOINTS.map(({ packageSubpath, packageExport }) => [
|
|
67
|
+
packageSubpath,
|
|
68
|
+
typeof packageExport === 'string' ? packageExport : { ...packageExport },
|
|
69
|
+
]),
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createOpenForgePluginSdkTypeScriptPaths() {
|
|
74
|
+
return Object.fromEntries(
|
|
75
|
+
OPENFORGE_PLUGIN_SDK_PUBLIC_ENTRYPOINTS.map(({ importSpecifier, workspaceSourcePath }) => [
|
|
76
|
+
importSpecifier,
|
|
77
|
+
[`./${workspaceSourcePath}`],
|
|
78
|
+
]),
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function loadOpenForgePluginSdkTypeScriptPaths(workspaceRoot) {
|
|
83
|
+
const typeScriptConfig = JSON.parse(
|
|
84
|
+
(await readFile(resolve(workspaceRoot, 'tsconfig.json'), 'utf8')).replace(/\/\*[\s\S]*?\*\//g, ''),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return Object.fromEntries(
|
|
88
|
+
Object.entries(typeScriptConfig.compilerOptions.paths)
|
|
89
|
+
.filter(([specifier]) => specifier === PLUGIN_SDK_PACKAGE_NAME || specifier.startsWith(`${PLUGIN_SDK_PACKAGE_NAME}/`)),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function assertOpenForgePluginSdkEntrypointRegistries({ packageExports, typeScriptPaths }) {
|
|
94
|
+
assertRegistryMatchesCanonicalManifest({
|
|
95
|
+
registryName: 'Plugin SDK package exports',
|
|
96
|
+
actual: packageExports,
|
|
97
|
+
expected: createOpenForgePluginSdkPackageExports(),
|
|
98
|
+
})
|
|
99
|
+
assertRegistryMatchesCanonicalManifest({
|
|
100
|
+
registryName: 'Plugin SDK root TypeScript paths',
|
|
101
|
+
actual: typeScriptPaths,
|
|
102
|
+
expected: createOpenForgePluginSdkTypeScriptPaths(),
|
|
103
|
+
})
|
|
104
|
+
}
|
package/dist/publicUiExports.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { assertRegistryMatchesCanonicalManifest } from './registryValidation.mjs'
|
|
2
|
+
|
|
1
3
|
const PLUGIN_SDK_PACKAGE_NAME = '@openforge-app/plugin-sdk'
|
|
2
4
|
|
|
3
5
|
const PUBLIC_UI_COMPONENT_NAMES = Object.freeze([
|
|
@@ -7,9 +9,11 @@ const PUBLIC_UI_COMPONENT_NAMES = Object.freeze([
|
|
|
7
9
|
'ResizablePanel',
|
|
8
10
|
'Modal',
|
|
9
11
|
'PluginPageHeader',
|
|
12
|
+
'PluginPageShell',
|
|
10
13
|
'PluginViewState',
|
|
11
14
|
'PluginSidebarLink',
|
|
12
15
|
'FileTypeIcon',
|
|
16
|
+
'ProjectFileTree',
|
|
13
17
|
'CollapsibleSection',
|
|
14
18
|
])
|
|
15
19
|
|
|
@@ -36,26 +40,12 @@ export function createOpenForgePluginSdkPublicUiPackageExports() {
|
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
export function assertOpenForgePluginSdkPublicUiPackageExports(packageExports) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
)
|
|
47
|
-
const expectedEntries = Object.entries(expected)
|
|
48
|
-
const missingOrMismatched = expectedEntries
|
|
49
|
-
.filter(([subpath, distPath]) => actual[subpath] !== distPath)
|
|
50
|
-
.map(([subpath, distPath]) => `${subpath} -> ${distPath}`)
|
|
51
|
-
const unexpected = Object.keys(actual).filter((subpath) => !(subpath in expected))
|
|
52
|
-
|
|
53
|
-
if (missingOrMismatched.length === 0 && unexpected.length === 0) return
|
|
54
|
-
|
|
55
|
-
const details = [
|
|
56
|
-
missingOrMismatched.length > 0 ? `missing or mismatched: ${missingOrMismatched.join(', ')}` : null,
|
|
57
|
-
unexpected.length > 0 ? `not in the canonical manifest: ${unexpected.join(', ')}` : null,
|
|
58
|
-
].filter(Boolean)
|
|
59
|
-
|
|
60
|
-
throw new Error(`Plugin SDK public UI exports drifted from the canonical manifest (${details.join('; ')})`)
|
|
43
|
+
assertRegistryMatchesCanonicalManifest({
|
|
44
|
+
registryName: 'Plugin SDK public UI exports',
|
|
45
|
+
actual: packageExports,
|
|
46
|
+
expected: createOpenForgePluginSdkPublicUiPackageExports(),
|
|
47
|
+
invalidRegistryMessage: 'Plugin SDK package.json must define an exports object',
|
|
48
|
+
includeActualEntry: ([subpath]) => subpath.startsWith('./ui/'),
|
|
49
|
+
formatMissingOrMismatched: ([subpath, distPath]) => `${subpath} -> ${distPath}`,
|
|
50
|
+
})
|
|
61
51
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type Registry = Record<string, unknown>
|
|
2
|
+
type RegistryEntry = [string, unknown]
|
|
3
|
+
|
|
4
|
+
export function assertRegistryMatchesCanonicalManifest(options: {
|
|
5
|
+
registryName: string
|
|
6
|
+
actual: unknown
|
|
7
|
+
expected: Registry
|
|
8
|
+
invalidRegistryMessage?: string
|
|
9
|
+
includeActualEntry?: (entry: RegistryEntry) => boolean
|
|
10
|
+
formatMissingOrMismatched?: (entry: RegistryEntry) => string
|
|
11
|
+
}): void
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
2
|
+
|
|
3
|
+
export function assertRegistryMatchesCanonicalManifest({
|
|
4
|
+
registryName,
|
|
5
|
+
actual,
|
|
6
|
+
expected,
|
|
7
|
+
invalidRegistryMessage = `${registryName} must be an object`,
|
|
8
|
+
includeActualEntry = () => true,
|
|
9
|
+
formatMissingOrMismatched = ([key]) => key,
|
|
10
|
+
}) {
|
|
11
|
+
if (!actual || typeof actual !== 'object' || Array.isArray(actual)) {
|
|
12
|
+
throw new Error(invalidRegistryMessage)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const comparableActual = Object.fromEntries(Object.entries(actual).filter(includeActualEntry))
|
|
16
|
+
|
|
17
|
+
const missingOrMismatched = Object.entries(expected)
|
|
18
|
+
.filter(([key, value]) => !isDeepStrictEqual(comparableActual[key], value))
|
|
19
|
+
.map(formatMissingOrMismatched)
|
|
20
|
+
const unexpected = Object.keys(comparableActual).filter((key) => !(key in expected))
|
|
21
|
+
|
|
22
|
+
if (missingOrMismatched.length === 0 && unexpected.length === 0) return
|
|
23
|
+
|
|
24
|
+
const details = [
|
|
25
|
+
missingOrMismatched.length > 0 ? `missing or mismatched: ${missingOrMismatched.join(', ')}` : null,
|
|
26
|
+
unexpected.length > 0 ? `not in the canonical manifest: ${unexpected.join(', ')}` : null,
|
|
27
|
+
].filter(Boolean)
|
|
28
|
+
|
|
29
|
+
throw new Error(`${registryName} drifted from the canonical manifest (${details.join('; ')})`)
|
|
30
|
+
}
|
package/dist/sanitize.d.ts
CHANGED
package/dist/sanitize.js
CHANGED
|
@@ -11,3 +11,134 @@ export function sanitizeHtml(dirty) {
|
|
|
11
11
|
FORBID_ATTR: ['style'],
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
|
+
const SAFE_MERMAID_STYLE_PROPERTIES = new Set([
|
|
15
|
+
'color',
|
|
16
|
+
'dominant-baseline',
|
|
17
|
+
'fill',
|
|
18
|
+
'fill-opacity',
|
|
19
|
+
'font-family',
|
|
20
|
+
'font-size',
|
|
21
|
+
'font-style',
|
|
22
|
+
'font-weight',
|
|
23
|
+
'max-width',
|
|
24
|
+
'opacity',
|
|
25
|
+
'stroke',
|
|
26
|
+
'stroke-dasharray',
|
|
27
|
+
'stroke-dashoffset',
|
|
28
|
+
'stroke-linecap',
|
|
29
|
+
'stroke-linejoin',
|
|
30
|
+
'stroke-opacity',
|
|
31
|
+
'stroke-width',
|
|
32
|
+
'text-align',
|
|
33
|
+
'text-anchor',
|
|
34
|
+
'white-space',
|
|
35
|
+
]);
|
|
36
|
+
function getSafeMermaidStyleDeclarations(style) {
|
|
37
|
+
const safeDeclarations = [];
|
|
38
|
+
for (const property of Array.from(style)) {
|
|
39
|
+
if (!SAFE_MERMAID_STYLE_PROPERTIES.has(property))
|
|
40
|
+
continue;
|
|
41
|
+
const value = style.getPropertyValue(property);
|
|
42
|
+
if (/url\s*\(|expression\s*\(|javascript:|data:|@import|var\s*\(/i.test(value))
|
|
43
|
+
continue;
|
|
44
|
+
const priority = style.getPropertyPriority(property);
|
|
45
|
+
safeDeclarations.push(`${property}: ${value}${priority ? ` !${priority}` : ''}`);
|
|
46
|
+
}
|
|
47
|
+
return safeDeclarations;
|
|
48
|
+
}
|
|
49
|
+
function removeCssAtRules(css) {
|
|
50
|
+
let result = '';
|
|
51
|
+
let index = 0;
|
|
52
|
+
while (index < css.length) {
|
|
53
|
+
if (css[index] !== '@') {
|
|
54
|
+
result += css[index++];
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
let depth = 0;
|
|
58
|
+
let quote = '';
|
|
59
|
+
for (; index < css.length; index++) {
|
|
60
|
+
const character = css[index];
|
|
61
|
+
if (quote) {
|
|
62
|
+
if (character === '\\')
|
|
63
|
+
index++;
|
|
64
|
+
else if (character === quote)
|
|
65
|
+
quote = '';
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (character === '"' || character === "'") {
|
|
69
|
+
quote = character;
|
|
70
|
+
}
|
|
71
|
+
else if (character === '{') {
|
|
72
|
+
depth++;
|
|
73
|
+
}
|
|
74
|
+
else if (character === '}') {
|
|
75
|
+
if (--depth <= 0) {
|
|
76
|
+
index++;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (character === ';' && depth === 0) {
|
|
81
|
+
index++;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
function sanitizeMermaidStylesheet(css, styleParser) {
|
|
89
|
+
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
90
|
+
const safeRuleSource = removeCssAtRules(withoutComments);
|
|
91
|
+
const rules = [];
|
|
92
|
+
const rulePattern = /([^{}]+)\{([^{}]*)\}/g;
|
|
93
|
+
let consumedUntil = 0;
|
|
94
|
+
let match;
|
|
95
|
+
while ((match = rulePattern.exec(safeRuleSource)) !== null) {
|
|
96
|
+
if (safeRuleSource.slice(consumedUntil, match.index).trim())
|
|
97
|
+
return '';
|
|
98
|
+
consumedUntil = rulePattern.lastIndex;
|
|
99
|
+
const selector = match[1].trim();
|
|
100
|
+
if (!selector || /[{}@\\]/.test(selector))
|
|
101
|
+
continue;
|
|
102
|
+
styleParser.setAttribute('style', match[2]);
|
|
103
|
+
const declarations = getSafeMermaidStyleDeclarations(styleParser.style);
|
|
104
|
+
if (declarations.length > 0)
|
|
105
|
+
rules.push(`${selector} { ${declarations.join('; ')} }`);
|
|
106
|
+
}
|
|
107
|
+
if (safeRuleSource.slice(consumedUntil).trim())
|
|
108
|
+
return '';
|
|
109
|
+
return rules.join('\n');
|
|
110
|
+
}
|
|
111
|
+
function sanitizeMermaidStyles(svg) {
|
|
112
|
+
const template = document.createElement('template');
|
|
113
|
+
template.innerHTML = svg;
|
|
114
|
+
const styleParser = document.createElement('span');
|
|
115
|
+
for (const element of template.content.querySelectorAll('[style]')) {
|
|
116
|
+
styleParser.setAttribute('style', element.getAttribute('style') ?? '');
|
|
117
|
+
const safeDeclarations = getSafeMermaidStyleDeclarations(styleParser.style);
|
|
118
|
+
if (safeDeclarations.length > 0) {
|
|
119
|
+
element.setAttribute('style', safeDeclarations.join('; '));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
element.removeAttribute('style');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const style of template.content.querySelectorAll('style')) {
|
|
126
|
+
const sanitized = sanitizeMermaidStylesheet(style.textContent ?? '', styleParser);
|
|
127
|
+
if (sanitized) {
|
|
128
|
+
style.textContent = sanitized;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
style.remove();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return template.innerHTML;
|
|
135
|
+
}
|
|
136
|
+
/** Sanitize generated Mermaid SVG before it is inserted into the document. */
|
|
137
|
+
export function sanitizeMermaidSvg(dirty) {
|
|
138
|
+
const sanitized = DOMPurify.sanitize(dirty, {
|
|
139
|
+
USE_PROFILES: { svg: true, svgFilters: true },
|
|
140
|
+
FORBID_TAGS: ['script', 'foreignObject', 'iframe', 'object', 'embed', 'image', 'a'],
|
|
141
|
+
FORBID_ATTR: ['href', 'xlink:href', 'target'],
|
|
142
|
+
});
|
|
143
|
+
return sanitizeMermaidStyles(sanitized);
|
|
144
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type TaskBrowserDevToolsShortcutPlatform = 'macos' | 'other';
|
|
2
|
+
export type TaskBrowserDevToolsShortcut = 'toggle' | 'elements' | 'console';
|
|
3
|
+
export interface TaskBrowserDevToolsShortcutInput {
|
|
4
|
+
key: string;
|
|
5
|
+
keyDown: boolean;
|
|
6
|
+
repeat: boolean;
|
|
7
|
+
control: boolean;
|
|
8
|
+
shift: boolean;
|
|
9
|
+
alt: boolean;
|
|
10
|
+
meta: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function classifyTaskBrowserDevToolsShortcut(platform: TaskBrowserDevToolsShortcutPlatform, input: TaskBrowserDevToolsShortcutInput): TaskBrowserDevToolsShortcut | null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function classifyTaskBrowserDevToolsShortcut(platform, input) {
|
|
2
|
+
if (!input.keyDown || input.repeat)
|
|
3
|
+
return null;
|
|
4
|
+
if (input.key === 'f12')
|
|
5
|
+
return 'toggle';
|
|
6
|
+
if (input.key === 'c') {
|
|
7
|
+
const elementsModified = platform === 'macos'
|
|
8
|
+
? input.meta && input.shift && !input.control && !input.alt
|
|
9
|
+
: input.control && input.shift && !input.meta && !input.alt;
|
|
10
|
+
return elementsModified ? 'elements' : null;
|
|
11
|
+
}
|
|
12
|
+
const modified = platform === 'macos'
|
|
13
|
+
? input.meta && input.alt && !input.control && !input.shift
|
|
14
|
+
: input.control && input.shift && !input.meta && !input.alt;
|
|
15
|
+
if (!modified)
|
|
16
|
+
return null;
|
|
17
|
+
if (input.key === 'i')
|
|
18
|
+
return 'toggle';
|
|
19
|
+
return input.key === 'j' ? 'console' : null;
|
|
20
|
+
}
|