Diffs: add viewer payload validation and presentation defaults
This commit is contained in:
@@ -92,7 +92,10 @@ Set plugin-wide defaults in `~/.openclaw/openclaw.json`:
|
|||||||
defaults: {
|
defaults: {
|
||||||
fontFamily: "Fira Code",
|
fontFamily: "Fira Code",
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
|
lineSpacing: 1.6,
|
||||||
layout: "unified",
|
layout: "unified",
|
||||||
|
showLineNumbers: true,
|
||||||
|
diffIndicators: "bars",
|
||||||
wordWrap: true,
|
wordWrap: true,
|
||||||
background: true,
|
background: true,
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
@@ -109,7 +112,10 @@ Supported defaults:
|
|||||||
|
|
||||||
- `fontFamily`
|
- `fontFamily`
|
||||||
- `fontSize`
|
- `fontSize`
|
||||||
|
- `lineSpacing`
|
||||||
- `layout`
|
- `layout`
|
||||||
|
- `showLineNumbers`
|
||||||
|
- `diffIndicators`
|
||||||
- `wordWrap`
|
- `wordWrap`
|
||||||
- `background`
|
- `background`
|
||||||
- `theme`
|
- `theme`
|
||||||
|
|||||||
@@ -68,7 +68,10 @@ Set plugin-wide defaults in `~/.openclaw/openclaw.json`:
|
|||||||
defaults: {
|
defaults: {
|
||||||
fontFamily: "Fira Code",
|
fontFamily: "Fira Code",
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
|
lineSpacing: 1.6,
|
||||||
layout: "unified",
|
layout: "unified",
|
||||||
|
showLineNumbers: true,
|
||||||
|
diffIndicators: "bars",
|
||||||
wordWrap: true,
|
wordWrap: true,
|
||||||
background: true,
|
background: true,
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -70,6 +70,9 @@ describe("diffs plugin registration", () => {
|
|||||||
theme: "light",
|
theme: "light",
|
||||||
background: false,
|
background: false,
|
||||||
layout: "split",
|
layout: "split",
|
||||||
|
showLineNumbers: false,
|
||||||
|
diffIndicators: "classic",
|
||||||
|
lineSpacing: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
runtime: {} as never,
|
runtime: {} as never,
|
||||||
@@ -119,5 +122,8 @@ describe("diffs plugin registration", () => {
|
|||||||
expect(String(res.body)).toContain('body data-theme="light"');
|
expect(String(res.body)).toContain('body data-theme="light"');
|
||||||
expect(String(res.body)).toContain('"backgroundEnabled":false');
|
expect(String(res.body)).toContain('"backgroundEnabled":false');
|
||||||
expect(String(res.body)).toContain('"diffStyle":"split"');
|
expect(String(res.body)).toContain('"diffStyle":"split"');
|
||||||
|
expect(String(res.body)).toContain('"disableLineNumbers":true');
|
||||||
|
expect(String(res.body)).toContain('"diffIndicators":"classic"');
|
||||||
|
expect(String(res.body)).toContain("--diffs-line-height: 30px;");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,10 +11,22 @@
|
|||||||
"label": "Default Font Size",
|
"label": "Default Font Size",
|
||||||
"help": "Base diff font size in pixels."
|
"help": "Base diff font size in pixels."
|
||||||
},
|
},
|
||||||
|
"defaults.lineSpacing": {
|
||||||
|
"label": "Default Line Spacing",
|
||||||
|
"help": "Line-height multiplier applied to diff rows."
|
||||||
|
},
|
||||||
"defaults.layout": {
|
"defaults.layout": {
|
||||||
"label": "Default Layout",
|
"label": "Default Layout",
|
||||||
"help": "Initial diff layout shown in the viewer."
|
"help": "Initial diff layout shown in the viewer."
|
||||||
},
|
},
|
||||||
|
"defaults.showLineNumbers": {
|
||||||
|
"label": "Show Line Numbers",
|
||||||
|
"help": "Show line numbers by default."
|
||||||
|
},
|
||||||
|
"defaults.diffIndicators": {
|
||||||
|
"label": "Diff Indicator Style",
|
||||||
|
"help": "Choose added/removed indicators style."
|
||||||
|
},
|
||||||
"defaults.wordWrap": {
|
"defaults.wordWrap": {
|
||||||
"label": "Default Word Wrap",
|
"label": "Default Word Wrap",
|
||||||
"help": "Wrap long lines by default."
|
"help": "Wrap long lines by default."
|
||||||
@@ -50,11 +62,26 @@
|
|||||||
"maximum": 24,
|
"maximum": 24,
|
||||||
"default": 15
|
"default": 15
|
||||||
},
|
},
|
||||||
|
"lineSpacing": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 3,
|
||||||
|
"default": 1.6
|
||||||
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["unified", "split"],
|
"enum": ["unified", "split"],
|
||||||
"default": "unified"
|
"default": "unified"
|
||||||
},
|
},
|
||||||
|
"showLineNumbers": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"diffIndicators": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["bars", "classic", "none"],
|
||||||
|
"default": "bars"
|
||||||
|
},
|
||||||
"wordWrap": {
|
"wordWrap": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true
|
"default": true
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ describe("resolveDiffsPluginDefaults", () => {
|
|||||||
defaults: {
|
defaults: {
|
||||||
fontFamily: "JetBrains Mono",
|
fontFamily: "JetBrains Mono",
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
|
lineSpacing: 1.8,
|
||||||
layout: "split",
|
layout: "split",
|
||||||
|
showLineNumbers: false,
|
||||||
|
diffIndicators: "classic",
|
||||||
wordWrap: false,
|
wordWrap: false,
|
||||||
background: false,
|
background: false,
|
||||||
theme: "light",
|
theme: "light",
|
||||||
@@ -22,11 +25,48 @@ describe("resolveDiffsPluginDefaults", () => {
|
|||||||
).toEqual({
|
).toEqual({
|
||||||
fontFamily: "JetBrains Mono",
|
fontFamily: "JetBrains Mono",
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
|
lineSpacing: 1.8,
|
||||||
layout: "split",
|
layout: "split",
|
||||||
|
showLineNumbers: false,
|
||||||
|
diffIndicators: "classic",
|
||||||
wordWrap: false,
|
wordWrap: false,
|
||||||
background: false,
|
background: false,
|
||||||
theme: "light",
|
theme: "light",
|
||||||
mode: "view",
|
mode: "view",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clamps and falls back for invalid line spacing and indicators", () => {
|
||||||
|
expect(
|
||||||
|
resolveDiffsPluginDefaults({
|
||||||
|
defaults: {
|
||||||
|
lineSpacing: -5,
|
||||||
|
diffIndicators: "unknown",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
lineSpacing: 1,
|
||||||
|
diffIndicators: "bars",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveDiffsPluginDefaults({
|
||||||
|
defaults: {
|
||||||
|
lineSpacing: 9,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
lineSpacing: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveDiffsPluginDefaults({
|
||||||
|
defaults: {
|
||||||
|
lineSpacing: Number.NaN,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
lineSpacing: DEFAULT_DIFFS_TOOL_DEFAULTS.lineSpacing,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk";
|
||||||
import {
|
import {
|
||||||
|
DIFF_INDICATORS,
|
||||||
DIFF_LAYOUTS,
|
DIFF_LAYOUTS,
|
||||||
DIFF_MODES,
|
DIFF_MODES,
|
||||||
DIFF_THEMES,
|
DIFF_THEMES,
|
||||||
|
type DiffIndicators,
|
||||||
type DiffLayout,
|
type DiffLayout,
|
||||||
type DiffMode,
|
type DiffMode,
|
||||||
type DiffPresentationDefaults,
|
type DiffPresentationDefaults,
|
||||||
@@ -14,7 +16,10 @@ type DiffsPluginConfig = {
|
|||||||
defaults?: {
|
defaults?: {
|
||||||
fontFamily?: string;
|
fontFamily?: string;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
|
lineSpacing?: number;
|
||||||
layout?: DiffLayout;
|
layout?: DiffLayout;
|
||||||
|
showLineNumbers?: boolean;
|
||||||
|
diffIndicators?: DiffIndicators;
|
||||||
wordWrap?: boolean;
|
wordWrap?: boolean;
|
||||||
background?: boolean;
|
background?: boolean;
|
||||||
theme?: DiffTheme;
|
theme?: DiffTheme;
|
||||||
@@ -25,7 +30,10 @@ type DiffsPluginConfig = {
|
|||||||
export const DEFAULT_DIFFS_TOOL_DEFAULTS: DiffToolDefaults = {
|
export const DEFAULT_DIFFS_TOOL_DEFAULTS: DiffToolDefaults = {
|
||||||
fontFamily: "Fira Code",
|
fontFamily: "Fira Code",
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
|
lineSpacing: 1.6,
|
||||||
layout: "unified",
|
layout: "unified",
|
||||||
|
showLineNumbers: true,
|
||||||
|
diffIndicators: "bars",
|
||||||
wordWrap: true,
|
wordWrap: true,
|
||||||
background: true,
|
background: true,
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
@@ -47,11 +55,26 @@ const DIFFS_PLUGIN_CONFIG_JSON_SCHEMA = {
|
|||||||
maximum: 24,
|
maximum: 24,
|
||||||
default: DEFAULT_DIFFS_TOOL_DEFAULTS.fontSize,
|
default: DEFAULT_DIFFS_TOOL_DEFAULTS.fontSize,
|
||||||
},
|
},
|
||||||
|
lineSpacing: {
|
||||||
|
type: "number",
|
||||||
|
minimum: 1,
|
||||||
|
maximum: 3,
|
||||||
|
default: DEFAULT_DIFFS_TOOL_DEFAULTS.lineSpacing,
|
||||||
|
},
|
||||||
layout: {
|
layout: {
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: [...DIFF_LAYOUTS],
|
enum: [...DIFF_LAYOUTS],
|
||||||
default: DEFAULT_DIFFS_TOOL_DEFAULTS.layout,
|
default: DEFAULT_DIFFS_TOOL_DEFAULTS.layout,
|
||||||
},
|
},
|
||||||
|
showLineNumbers: {
|
||||||
|
type: "boolean",
|
||||||
|
default: DEFAULT_DIFFS_TOOL_DEFAULTS.showLineNumbers,
|
||||||
|
},
|
||||||
|
diffIndicators: {
|
||||||
|
type: "string",
|
||||||
|
enum: [...DIFF_INDICATORS],
|
||||||
|
default: DEFAULT_DIFFS_TOOL_DEFAULTS.diffIndicators,
|
||||||
|
},
|
||||||
wordWrap: { type: "boolean", default: DEFAULT_DIFFS_TOOL_DEFAULTS.wordWrap },
|
wordWrap: { type: "boolean", default: DEFAULT_DIFFS_TOOL_DEFAULTS.wordWrap },
|
||||||
background: { type: "boolean", default: DEFAULT_DIFFS_TOOL_DEFAULTS.background },
|
background: { type: "boolean", default: DEFAULT_DIFFS_TOOL_DEFAULTS.background },
|
||||||
theme: {
|
theme: {
|
||||||
@@ -101,7 +124,10 @@ export function resolveDiffsPluginDefaults(config: unknown): DiffToolDefaults {
|
|||||||
return {
|
return {
|
||||||
fontFamily: normalizeFontFamily(defaults.fontFamily),
|
fontFamily: normalizeFontFamily(defaults.fontFamily),
|
||||||
fontSize: normalizeFontSize(defaults.fontSize),
|
fontSize: normalizeFontSize(defaults.fontSize),
|
||||||
|
lineSpacing: normalizeLineSpacing(defaults.lineSpacing),
|
||||||
layout: normalizeLayout(defaults.layout),
|
layout: normalizeLayout(defaults.layout),
|
||||||
|
showLineNumbers: defaults.showLineNumbers !== false,
|
||||||
|
diffIndicators: normalizeDiffIndicators(defaults.diffIndicators),
|
||||||
wordWrap: defaults.wordWrap !== false,
|
wordWrap: defaults.wordWrap !== false,
|
||||||
background: defaults.background !== false,
|
background: defaults.background !== false,
|
||||||
theme: normalizeTheme(defaults.theme),
|
theme: normalizeTheme(defaults.theme),
|
||||||
@@ -110,11 +136,24 @@ export function resolveDiffsPluginDefaults(config: unknown): DiffToolDefaults {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toPresentationDefaults(defaults: DiffToolDefaults): DiffPresentationDefaults {
|
export function toPresentationDefaults(defaults: DiffToolDefaults): DiffPresentationDefaults {
|
||||||
const { fontFamily, fontSize, layout, wordWrap, background, theme } = defaults;
|
const {
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
lineSpacing,
|
||||||
|
layout,
|
||||||
|
showLineNumbers,
|
||||||
|
diffIndicators,
|
||||||
|
wordWrap,
|
||||||
|
background,
|
||||||
|
theme,
|
||||||
|
} = defaults;
|
||||||
return {
|
return {
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize,
|
fontSize,
|
||||||
|
lineSpacing,
|
||||||
layout,
|
layout,
|
||||||
|
showLineNumbers,
|
||||||
|
diffIndicators,
|
||||||
wordWrap,
|
wordWrap,
|
||||||
background,
|
background,
|
||||||
theme,
|
theme,
|
||||||
@@ -134,10 +173,23 @@ function normalizeFontSize(fontSize?: number): number {
|
|||||||
return Math.min(Math.max(rounded, 10), 24);
|
return Math.min(Math.max(rounded, 10), 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeLineSpacing(lineSpacing?: number): number {
|
||||||
|
if (lineSpacing === undefined || !Number.isFinite(lineSpacing)) {
|
||||||
|
return DEFAULT_DIFFS_TOOL_DEFAULTS.lineSpacing;
|
||||||
|
}
|
||||||
|
return Math.min(Math.max(lineSpacing, 1), 3);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeLayout(layout?: DiffLayout): DiffLayout {
|
function normalizeLayout(layout?: DiffLayout): DiffLayout {
|
||||||
return layout && DIFF_LAYOUTS.includes(layout) ? layout : DEFAULT_DIFFS_TOOL_DEFAULTS.layout;
|
return layout && DIFF_LAYOUTS.includes(layout) ? layout : DEFAULT_DIFFS_TOOL_DEFAULTS.layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDiffIndicators(diffIndicators?: DiffIndicators): DiffIndicators {
|
||||||
|
return diffIndicators && DIFF_INDICATORS.includes(diffIndicators)
|
||||||
|
? diffIndicators
|
||||||
|
: DEFAULT_DIFFS_TOOL_DEFAULTS.diffIndicators;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeTheme(theme?: DiffTheme): DiffTheme {
|
function normalizeTheme(theme?: DiffTheme): DiffTheme {
|
||||||
return theme && DIFF_THEMES.includes(theme) ? theme : DEFAULT_DIFFS_TOOL_DEFAULTS.theme;
|
return theme && DIFF_THEMES.includes(theme) ? theme : DEFAULT_DIFFS_TOOL_DEFAULTS.theme;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ describe("renderDiffDocument", () => {
|
|||||||
expect(rendered.imageHtml).toContain('data-openclaw-diffs-ready="true"');
|
expect(rendered.imageHtml).toContain('data-openclaw-diffs-ready="true"');
|
||||||
expect(rendered.imageHtml).toContain("max-width: 960px;");
|
expect(rendered.imageHtml).toContain("max-width: 960px;");
|
||||||
expect(rendered.imageHtml).toContain("--diffs-font-size: 16px;");
|
expect(rendered.imageHtml).toContain("--diffs-font-size: 16px;");
|
||||||
|
expect(rendered.html).toContain('"diffIndicators":"bars"');
|
||||||
|
expect(rendered.html).toContain('"disableLineNumbers":false');
|
||||||
|
expect(rendered.html).toContain("--diffs-line-height: 24px;");
|
||||||
expect(rendered.html).toContain("--diffs-font-size: 15px;");
|
expect(rendered.html).toContain("--diffs-font-size: 15px;");
|
||||||
expect(rendered.html).not.toContain("fonts.googleapis.com");
|
expect(rendered.html).not.toContain("fonts.googleapis.com");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,13 +52,15 @@ function resolveBeforeAfterFileName(input: Extract<DiffInput, { kind: "before_af
|
|||||||
function buildDiffOptions(options: DiffRenderOptions): DiffViewerOptions {
|
function buildDiffOptions(options: DiffRenderOptions): DiffViewerOptions {
|
||||||
const fontFamily = escapeCssString(options.presentation.fontFamily);
|
const fontFamily = escapeCssString(options.presentation.fontFamily);
|
||||||
const fontSize = Math.max(10, Math.floor(options.presentation.fontSize));
|
const fontSize = Math.max(10, Math.floor(options.presentation.fontSize));
|
||||||
const lineHeight = Math.max(20, Math.round(fontSize * 1.6));
|
const lineHeight = Math.max(20, Math.round(fontSize * options.presentation.lineSpacing));
|
||||||
return {
|
return {
|
||||||
theme: {
|
theme: {
|
||||||
light: "pierre-light",
|
light: "pierre-light",
|
||||||
dark: "pierre-dark",
|
dark: "pierre-dark",
|
||||||
},
|
},
|
||||||
diffStyle: options.presentation.layout,
|
diffStyle: options.presentation.layout,
|
||||||
|
diffIndicators: options.presentation.diffIndicators,
|
||||||
|
disableLineNumbers: !options.presentation.showLineNumbers,
|
||||||
expandUnchanged: options.expandUnchanged,
|
expandUnchanged: options.expandUnchanged,
|
||||||
themeType: options.presentation.theme,
|
themeType: options.presentation.theme,
|
||||||
backgroundEnabled: options.presentation.background,
|
backgroundEnabled: options.presentation.background,
|
||||||
|
|||||||
@@ -3,15 +3,20 @@ import type { FileContents, FileDiffMetadata, SupportedLanguages } from "@pierre
|
|||||||
export const DIFF_LAYOUTS = ["unified", "split"] as const;
|
export const DIFF_LAYOUTS = ["unified", "split"] as const;
|
||||||
export const DIFF_MODES = ["view", "image", "both"] as const;
|
export const DIFF_MODES = ["view", "image", "both"] as const;
|
||||||
export const DIFF_THEMES = ["light", "dark"] as const;
|
export const DIFF_THEMES = ["light", "dark"] as const;
|
||||||
|
export const DIFF_INDICATORS = ["bars", "classic", "none"] as const;
|
||||||
|
|
||||||
export type DiffLayout = (typeof DIFF_LAYOUTS)[number];
|
export type DiffLayout = (typeof DIFF_LAYOUTS)[number];
|
||||||
export type DiffMode = (typeof DIFF_MODES)[number];
|
export type DiffMode = (typeof DIFF_MODES)[number];
|
||||||
export type DiffTheme = (typeof DIFF_THEMES)[number];
|
export type DiffTheme = (typeof DIFF_THEMES)[number];
|
||||||
|
export type DiffIndicators = (typeof DIFF_INDICATORS)[number];
|
||||||
|
|
||||||
export type DiffPresentationDefaults = {
|
export type DiffPresentationDefaults = {
|
||||||
fontFamily: string;
|
fontFamily: string;
|
||||||
fontSize: number;
|
fontSize: number;
|
||||||
|
lineSpacing: number;
|
||||||
layout: DiffLayout;
|
layout: DiffLayout;
|
||||||
|
showLineNumbers: boolean;
|
||||||
|
diffIndicators: DiffIndicators;
|
||||||
wordWrap: boolean;
|
wordWrap: boolean;
|
||||||
background: boolean;
|
background: boolean;
|
||||||
theme: DiffTheme;
|
theme: DiffTheme;
|
||||||
@@ -49,6 +54,8 @@ export type DiffViewerOptions = {
|
|||||||
dark: "pierre-dark";
|
dark: "pierre-dark";
|
||||||
};
|
};
|
||||||
diffStyle: DiffLayout;
|
diffStyle: DiffLayout;
|
||||||
|
diffIndicators: DiffIndicators;
|
||||||
|
disableLineNumbers: boolean;
|
||||||
expandUnchanged: boolean;
|
expandUnchanged: boolean;
|
||||||
themeType: DiffTheme;
|
themeType: DiffTheme;
|
||||||
backgroundEnabled: boolean;
|
backgroundEnabled: boolean;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
SupportedLanguages,
|
SupportedLanguages,
|
||||||
} from "@pierre/diffs";
|
} from "@pierre/diffs";
|
||||||
import type { DiffViewerPayload, DiffLayout, DiffTheme } from "./types.js";
|
import type { DiffViewerPayload, DiffLayout, DiffTheme } from "./types.js";
|
||||||
|
import { parseViewerPayloadJson } from "./viewer-payload.js";
|
||||||
|
|
||||||
type ViewerState = {
|
type ViewerState = {
|
||||||
theme: DiffTheme;
|
theme: DiffTheme;
|
||||||
@@ -33,18 +34,25 @@ function parsePayload(element: HTMLScriptElement): DiffViewerPayload {
|
|||||||
if (!raw) {
|
if (!raw) {
|
||||||
throw new Error("Diff payload was empty.");
|
throw new Error("Diff payload was empty.");
|
||||||
}
|
}
|
||||||
return JSON.parse(raw) as DiffViewerPayload;
|
return parseViewerPayloadJson(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCards(): Array<{ host: HTMLElement; payload: DiffViewerPayload }> {
|
function getCards(): Array<{ host: HTMLElement; payload: DiffViewerPayload }> {
|
||||||
return [...document.querySelectorAll<HTMLElement>(".oc-diff-card")].flatMap((card) => {
|
const cards: Array<{ host: HTMLElement; payload: DiffViewerPayload }> = [];
|
||||||
|
for (const card of document.querySelectorAll<HTMLElement>(".oc-diff-card")) {
|
||||||
const host = card.querySelector<HTMLElement>("[data-openclaw-diff-host]");
|
const host = card.querySelector<HTMLElement>("[data-openclaw-diff-host]");
|
||||||
const payloadNode = card.querySelector<HTMLScriptElement>("[data-openclaw-diff-payload]");
|
const payloadNode = card.querySelector<HTMLScriptElement>("[data-openclaw-diff-payload]");
|
||||||
if (!host || !payloadNode) {
|
if (!host || !payloadNode) {
|
||||||
return [];
|
continue;
|
||||||
}
|
}
|
||||||
return [{ host, payload: parsePayload(payloadNode) }];
|
|
||||||
});
|
try {
|
||||||
|
cards.push({ host, payload: parsePayload(payloadNode) });
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Skipping invalid diff payload", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cards;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureShadowRoot(host: HTMLElement): void {
|
function ensureShadowRoot(host: HTMLElement): void {
|
||||||
@@ -249,8 +257,10 @@ function createRenderOptions(payload: DiffViewerPayload): FileDiffOptions<undefi
|
|||||||
theme: payload.options.theme,
|
theme: payload.options.theme,
|
||||||
themeType: viewerState.theme,
|
themeType: viewerState.theme,
|
||||||
diffStyle: viewerState.layout,
|
diffStyle: viewerState.layout,
|
||||||
|
diffIndicators: payload.options.diffIndicators,
|
||||||
expandUnchanged: payload.options.expandUnchanged,
|
expandUnchanged: payload.options.expandUnchanged,
|
||||||
overflow: viewerState.wrapEnabled ? "wrap" : "scroll",
|
overflow: viewerState.wrapEnabled ? "wrap" : "scroll",
|
||||||
|
disableLineNumbers: payload.options.disableLineNumbers,
|
||||||
disableBackground: !viewerState.backgroundEnabled,
|
disableBackground: !viewerState.backgroundEnabled,
|
||||||
unsafeCSS: payload.options.unsafeCSS,
|
unsafeCSS: payload.options.unsafeCSS,
|
||||||
renderHeaderMetadata: () => createToolbar(),
|
renderHeaderMetadata: () => createToolbar(),
|
||||||
|
|||||||
55
extensions/diffs/src/viewer-payload.test.ts
Normal file
55
extensions/diffs/src/viewer-payload.test.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { parseViewerPayloadJson } from "./viewer-payload.js";
|
||||||
|
|
||||||
|
function buildValidPayload(): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
prerenderedHTML: "<div>ok</div>",
|
||||||
|
langs: ["text"],
|
||||||
|
oldFile: {
|
||||||
|
name: "README.md",
|
||||||
|
contents: "before",
|
||||||
|
},
|
||||||
|
newFile: {
|
||||||
|
name: "README.md",
|
||||||
|
contents: "after",
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
theme: {
|
||||||
|
light: "pierre-light",
|
||||||
|
dark: "pierre-dark",
|
||||||
|
},
|
||||||
|
diffStyle: "unified",
|
||||||
|
diffIndicators: "bars",
|
||||||
|
disableLineNumbers: false,
|
||||||
|
expandUnchanged: false,
|
||||||
|
themeType: "dark",
|
||||||
|
backgroundEnabled: true,
|
||||||
|
overflow: "wrap",
|
||||||
|
unsafeCSS: ":host{}",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("parseViewerPayloadJson", () => {
|
||||||
|
it("accepts valid payload JSON", () => {
|
||||||
|
const parsed = parseViewerPayloadJson(JSON.stringify(buildValidPayload()));
|
||||||
|
expect(parsed.options.diffStyle).toBe("unified");
|
||||||
|
expect(parsed.options.diffIndicators).toBe("bars");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects payloads with invalid shape", () => {
|
||||||
|
const broken = buildValidPayload();
|
||||||
|
broken.options = {
|
||||||
|
...(broken.options as Record<string, unknown>),
|
||||||
|
diffIndicators: "invalid",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => parseViewerPayloadJson(JSON.stringify(broken))).toThrow(
|
||||||
|
"Diff payload has invalid shape.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid JSON", () => {
|
||||||
|
expect(() => parseViewerPayloadJson("{not-json")).toThrow("Diff payload is not valid JSON.");
|
||||||
|
});
|
||||||
|
});
|
||||||
94
extensions/diffs/src/viewer-payload.ts
Normal file
94
extensions/diffs/src/viewer-payload.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { DIFF_INDICATORS, DIFF_LAYOUTS, DIFF_THEMES } from "./types.js";
|
||||||
|
import type { DiffViewerPayload } from "./types.js";
|
||||||
|
|
||||||
|
const OVERFLOW_VALUES = ["scroll", "wrap"] as const;
|
||||||
|
|
||||||
|
export function parseViewerPayloadJson(raw: string): DiffViewerPayload {
|
||||||
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
throw new Error("Diff payload is not valid JSON.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDiffViewerPayload(parsed)) {
|
||||||
|
throw new Error("Diff payload has invalid shape.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDiffViewerPayload(value: unknown): value is DiffViewerPayload {
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value.prerenderedHTML !== "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(value.langs) || !value.langs.every((lang) => typeof lang === "string")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isViewerOptions(value.options)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFileDiff = isRecord(value.fileDiff);
|
||||||
|
const hasBeforeAfterFiles = isRecord(value.oldFile) && isRecord(value.newFile);
|
||||||
|
if (!hasFileDiff && !hasBeforeAfterFiles) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isViewerOptions(value: unknown): boolean {
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRecord(value.theme)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (value.theme.light !== "pierre-light" || value.theme.dark !== "pierre-dark") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!includesValue(DIFF_LAYOUTS, value.diffStyle)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!includesValue(DIFF_INDICATORS, value.diffIndicators)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!includesValue(DIFF_THEMES, value.themeType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!includesValue(OVERFLOW_VALUES, value.overflow)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value.disableLineNumbers !== "boolean") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof value.expandUnchanged !== "boolean") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof value.backgroundEnabled !== "boolean") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof value.unsafeCSS !== "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function includesValue<T extends readonly string[]>(values: T, value: unknown): value is T[number] {
|
||||||
|
return typeof value === "string" && values.includes(value as T[number]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user