Skip to content

Commit 8964bb4

Browse files
authored
Merge pull request #136 from DiamondLightSource/parse-embedded-scripts
Add capability to extract embedded scripts from bob files
2 parents 507016f + 0a3a067 commit 8964bb4

File tree

7 files changed

+337
-5
lines changed

7 files changed

+337
-5
lines changed

src/types/props.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type GenericProp =
2222
| Border
2323
| Position
2424
| Rule[]
25+
| Script[]
2526
| MacroMap
2627
| WidgetActions
2728
| OpiFile
@@ -43,6 +44,8 @@ interface RulePV {
4344
trigger: boolean;
4445
}
4546

47+
type ScriptPV = RulePV;
48+
4649
export interface Rule {
4750
name: string;
4851
prop: string;
@@ -51,6 +54,12 @@ export interface Rule {
5154
expressions: Expression[];
5255
}
5356

57+
export interface Script {
58+
file: string;
59+
pvs: ScriptPV[];
60+
text: string;
61+
}
62+
5463
export interface OpiFile {
5564
path: string;
5665
macros: MacroMap;

src/ui/widgets/EmbeddedDisplay/bobParser.test.ts

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ describe("bob widget parser", (): void => {
6464
expect(widget.rotationStep).toEqual(1);
6565
});
6666

67-
const readbackString = `
67+
const readbackStringWithEmbeddedScript = `
6868
<display version="2.0.0">
6969
<x>0</x>
7070
<y>0</y>
@@ -78,14 +78,76 @@ describe("bob widget parser", (): void => {
7878
<width>140</width>
7979
<height>50</height>
8080
<border_alarm_sensitive>false</border_alarm_sensitive>
81+
<scripts>
82+
<script file="EmbeddedJs">
83+
<text>
84+
/* Embedded javascript */ importClass(org.csstudio.display.builder.runtime.script.PVUtil);
85+
importClass(org.csstudio.display.builder.runtime.script.ScriptUtil);
86+
importPackage(Packages.org.csstudio.opibuilder.scriptUtil);
87+
logger = ScriptUtil.getLogger();
88+
logger.info("Hello")
89+
var value = PVUtil.getDouble(pvs[0]);
90+
if (value > 299) {
91+
widget.setPropertyValue("background_color", ColorFontUtil.getColorFromRGB(255, 255, 0));
92+
} else {
93+
widget.setPropertyValue("background_color", ColorFontUtil.getColorFromRGB(128, 255, 255));
94+
}
95+
</text>
96+
<pv_name>SR-DI-DCCT-01:SIGNAL</pv_name>
97+
<pv_name trigger="false">$(pv_name)</pv_name>
98+
</script>
99+
</scripts>
81100
</widget>
82101
</display>`;
83102
it("parses a readback widget", async (): Promise<void> => {
84-
const widget = (await parseBob(readbackString, "xxx", PREFIX))
85-
.children?.[0] as WidgetDescription;
103+
const widget = (
104+
await parseBob(readbackStringWithEmbeddedScript, "xxx", PREFIX)
105+
).children?.[0] as WidgetDescription;
86106
expect(widget.pvMetadataList[0].pvName).toEqual(PV.parse("xxx://abc"));
87107
});
88108

109+
it("parses an embeded script in a widget", async (): Promise<void> => {
110+
const expectedScripts = [
111+
{
112+
file: "EmbeddedJs",
113+
pvs: [
114+
{
115+
pvName: {
116+
name: "SR-DI-DCCT-01:SIGNAL",
117+
protocol: "xxx"
118+
},
119+
trigger: true
120+
},
121+
{
122+
pvName: {
123+
name: "$(pv_name)",
124+
protocol: "xxx"
125+
},
126+
trigger: true
127+
}
128+
],
129+
text: `
130+
/* Embedded javascript */ importClass(org.csstudio.display.builder.runtime.script.PVUtil);
131+
importClass(org.csstudio.display.builder.runtime.script.ScriptUtil);
132+
importPackage(Packages.org.csstudio.opibuilder.scriptUtil);
133+
logger = ScriptUtil.getLogger();
134+
logger.info("Hello")
135+
var value = PVUtil.getDouble(pvs[0]);
136+
if (value > 299) {
137+
widget.setPropertyValue("background_color", ColorFontUtil.getColorFromRGB(255, 255, 0));
138+
} else {
139+
widget.setPropertyValue("background_color", ColorFontUtil.getColorFromRGB(128, 255, 255));
140+
}
141+
`
142+
}
143+
];
144+
145+
const widget = (
146+
await parseBob(readbackStringWithEmbeddedScript, "xxx", PREFIX)
147+
).children?.[0] as WidgetDescription;
148+
expect(widget.scripts).toEqual(expectedScripts);
149+
});
150+
89151
const noXString = `
90152
<display version="2.0.0">
91153
<y>0</y>

src/ui/widgets/EmbeddedDisplay/bobParser.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
RelativePosition
2727
} from "../../../types/position";
2828
import { PV } from "../../../types/pv";
29-
import { OpiFile, Rule } from "../../../types/props";
29+
import { OpiFile, Rule, Script } from "../../../types/props";
3030
import {
3131
OPEN_PAGE,
3232
OPEN_TAB,
@@ -42,6 +42,7 @@ import { Point, Points } from "../../../types/points";
4242
import { Axis } from "../../../types/axis";
4343
import { Trace } from "../../../types/trace";
4444
import { parsePlt } from "./pltParser";
45+
import { scriptParser } from "./scripts/scriptParser";
4546

4647
const BOB_WIDGET_MAPPING: { [key: string]: any } = {
4748
action_button: "actionbutton",
@@ -506,6 +507,8 @@ export async function parseBob(
506507
...BOB_COMPLEX_PARSERS,
507508
rules: (rules: Rule[]): Rule[] =>
508509
opiParseRules(rules, defaultProtocol, false),
510+
scripts: (scripts: ElementCompact): Script[] =>
511+
scriptParser(scripts, defaultProtocol, false),
509512
traces: (props: ElementCompact) => bobParseTraces(props["traces"]),
510513
axes: (props: ElementCompact) => bobParseYAxes(props["y_axes"]),
511514
plt: async (props: ElementCompact) =>
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
import { scriptParser } from "./scriptParser";
3+
import { ElementCompact } from "xml-js";
4+
import * as opiParserModule from "../opiParser";
5+
import { PV } from "../../../../types";
6+
7+
vi.mock("../opiParser", () => ({
8+
opiParsePvName: vi.fn()
9+
}));
10+
11+
describe("scriptParser", () => {
12+
const PV1 = new PV("pv1:processed", "ca");
13+
const PV2 = new PV("pv2:processed", "ca");
14+
15+
beforeEach(() => {
16+
vi.clearAllMocks();
17+
vi.mocked(opiParserModule.opiParsePvName)
18+
.mockReturnValueOnce(PV1)
19+
.mockReturnValueOnce(PV2);
20+
});
21+
22+
it("should return an empty array when jsonProp has no scripts", () => {
23+
const jsonProp: ElementCompact = {};
24+
const result = scriptParser(jsonProp, "ca", true);
25+
expect(result).toEqual([]);
26+
});
27+
28+
it("should parse OPI file scripts correctly", () => {
29+
const pv1 = { _attributes: { trig: "true" } };
30+
const pv2 = { _attributes: { trig: "false" } };
31+
32+
const scriptElement = {
33+
script: {
34+
_attributes: { file: "test.js" },
35+
text: { _cdata: 'console.log("Hello")' },
36+
pv: [pv1, pv2]
37+
}
38+
};
39+
40+
const jsonProp: ElementCompact = {
41+
scripts: [scriptElement]
42+
};
43+
44+
const result = scriptParser(jsonProp, "ca", true);
45+
46+
expect(result).toEqual([
47+
{
48+
file: "test.js",
49+
text: 'console.log("Hello")',
50+
pvs: [
51+
{ pvName: PV1, trigger: true },
52+
{ pvName: PV2, trigger: false }
53+
]
54+
}
55+
]);
56+
57+
expect(opiParserModule.opiParsePvName).toHaveBeenCalledTimes(2);
58+
});
59+
60+
it("should parse non-OPI file scripts correctly", () => {
61+
const pv1 = { _attributes: {} };
62+
const pv2 = { _attributes: {} };
63+
64+
const scriptElement = {
65+
script: {
66+
_attributes: { file: "test.js" },
67+
text: { _text: 'console.log("Hello")' },
68+
pv_name: [pv1, pv2]
69+
}
70+
};
71+
72+
const jsonProp: ElementCompact = {
73+
scripts: [scriptElement]
74+
};
75+
76+
const result = scriptParser(jsonProp, "ca", false);
77+
78+
expect(result).toEqual([
79+
{
80+
file: "test.js",
81+
text: 'console.log("Hello")',
82+
pvs: [
83+
{ pvName: PV1, trigger: true },
84+
{ pvName: PV2, trigger: true }
85+
]
86+
}
87+
]);
88+
89+
expect(opiParserModule.opiParsePvName).toHaveBeenCalledTimes(2);
90+
});
91+
92+
it("should handle scripts with no file attribute", () => {
93+
const scriptElement = {
94+
script: {
95+
text: { _text: 'console.log("No file")' },
96+
pv: []
97+
}
98+
};
99+
100+
const jsonProp: ElementCompact = {
101+
scripts: [scriptElement]
102+
};
103+
104+
const result = scriptParser(jsonProp, "ca", true);
105+
106+
expect(result).toEqual([
107+
{
108+
file: undefined,
109+
text: 'console.log("No file")',
110+
pvs: []
111+
}
112+
]);
113+
});
114+
115+
it("should handle scripts with no text content", () => {
116+
const scriptElement = {
117+
script: {
118+
_attributes: { file: "empty.js" },
119+
pv: []
120+
}
121+
};
122+
123+
const jsonProp: ElementCompact = {
124+
scripts: [scriptElement]
125+
};
126+
127+
const result = scriptParser(jsonProp, "ca", true);
128+
129+
expect(result).toEqual([
130+
{
131+
file: "empty.js",
132+
text: "",
133+
pvs: []
134+
}
135+
]);
136+
});
137+
138+
it("should handle multiple scripts in the array", () => {
139+
const scriptElement1 = {
140+
script: {
141+
_attributes: { file: "script1.js" },
142+
text: { _cdata: 'print("Script 1")' },
143+
pv: []
144+
}
145+
};
146+
147+
const scriptElement2 = {
148+
script: {
149+
_attributes: { file: "script2.js" },
150+
text: { _cdata: 'print("Script 2")' },
151+
pv: []
152+
}
153+
};
154+
155+
const jsonProp: ElementCompact = {
156+
scripts: [scriptElement1, scriptElement2]
157+
};
158+
159+
const result = scriptParser(jsonProp, "ca", true);
160+
161+
expect(result).toEqual([
162+
{
163+
file: "script1.js",
164+
text: 'print("Script 1")',
165+
pvs: []
166+
},
167+
{
168+
file: "script2.js",
169+
text: 'print("Script 2")',
170+
pvs: []
171+
}
172+
]);
173+
});
174+
175+
it("should handle single script not in array format", () => {
176+
const scriptElement = {
177+
script: {
178+
_attributes: { file: "single.js" },
179+
text: { _text: 'print("Single")' },
180+
pv: []
181+
}
182+
};
183+
184+
const jsonProp: ElementCompact = {
185+
scripts: scriptElement
186+
};
187+
188+
const result = scriptParser(jsonProp, "ca", true);
189+
190+
expect(result).toEqual([
191+
{
192+
file: "single.js",
193+
text: 'print("Single")',
194+
pvs: []
195+
}
196+
]);
197+
});
198+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { ElementCompact } from "xml-js";
2+
import { Script } from "../../../../types/props";
3+
import { toArray } from "../parser";
4+
import { opiParsePvName } from "../opiParser";
5+
6+
/**
7+
* Returns a script object array from a json element
8+
* @param jsonProp The json describing the array of script objects
9+
* @param defaultProtocol The default protocol eg ca (channel access)
10+
* @param isOpiFile true if this is an OPI file, false otherwise.
11+
*/
12+
export const scriptParser = (
13+
jsonProp: ElementCompact,
14+
defaultProtocol: string,
15+
isOpiFile: boolean
16+
): Script[] => {
17+
if (!jsonProp.scripts) {
18+
return [];
19+
} else {
20+
const scriptsArray = toArray(jsonProp.scripts);
21+
22+
return scriptsArray.map((element: ElementCompact): Script => {
23+
const script = element?.script;
24+
25+
const file = script?._attributes?.file as string;
26+
const text = (script?.text?._cdata ??
27+
script?.text?._text ??
28+
"") as string;
29+
30+
const pvArray = toArray(isOpiFile ? script?.pv : script?.pv_name);
31+
const pvs = pvArray.map((pv: ElementCompact) => {
32+
return {
33+
pvName: opiParsePvName(pv, defaultProtocol),
34+
trigger: isOpiFile ? pv._attributes?.trig === "true" : true
35+
};
36+
});
37+
38+
return {
39+
file: file,
40+
text: text,
41+
pvs: pvs
42+
} as Script;
43+
});
44+
}
45+
};

0 commit comments

Comments
 (0)