Skip to content

Commit cdcec16

Browse files
authored
Instructions syntax highlighting for CEfE (#1190)
closes RaspberryPiFoundation/digital-editor-issues#418
1 parent 41724de commit cdcec16

6 files changed

Lines changed: 123 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
2424
- Dark mode for instuctions code block (#1187)
2525
- Change markdown links to open in new tab (#1188)
2626
- Update demo instructions text (#1189)
27+
- Syntax highlighting for custom instructions in Code Editor for Education (#1190)
2728

2829
### Changed
2930

@@ -35,7 +36,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
3536
### Fixed
3637

3738
- Fix AWS CLI in deploy script to 2.22.35 to workaround cloudflare issue (See https://developers.cloudflare.com/r2/examples/aws/aws-cli/) (#1178)
38-
- Padding on instructions code block (#1184)
39+
- Padding on instructions code block (#1184, 1190)
3940

4041
## [0.28.14] - 2025-01-06
4142

src/assets/stylesheets/Instructions.scss

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,24 @@
4040
}
4141
}
4242

43+
code {
44+
color: $rpf-white;
45+
background-color: $rpf-grey-700;
46+
border-radius: 8px;
47+
padding: calc(0.75 * $space-0-125) $space-0-5;
48+
}
49+
4350
pre {
4451
background-color: $rpf-grey-700;
4552
border: 1px solid $rpf-grey-600;
4653
border-radius: 8px;
4754
padding: $space-0-5 $space-1;
4855
overflow: auto;
4956
margin: $space-1 0;
50-
}
5157

52-
code {
53-
color: $rpf-white;
54-
background-color: $rpf-grey-700;
55-
border-radius: 8px;
56-
padding-block: calc(0.75 * $space-0-125);
58+
code {
59+
padding-inline: 0;
60+
}
5761
}
5862

5963
.c-project-code {
@@ -125,6 +129,27 @@
125129
}
126130
}
127131

132+
.language-javascript {
133+
.number,
134+
.boolean {
135+
color: $rpf-syntax-1;
136+
}
137+
.keyword {
138+
color: $rpf-syntax-4;
139+
}
140+
.string,
141+
.char {
142+
color: $rpf-syntax-2;
143+
}
144+
.comment {
145+
color: $rpf-syntax-3;
146+
}
147+
148+
.keyword-print {
149+
color: $rpf-white;
150+
}
151+
}
152+
128153
.language-css {
129154
color: $rpf-syntax-1;
130155

src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import DesignSystemButton from "../../../DesignSystemButton/DesignSystemButton";
1818
import { setProjectInstructions } from "../../../../redux/EditorSlice";
1919
import demoInstructions from "../../../../assets/markdown/demoInstructions.md";
2020
import RemoveInstructionsModal from "../../../Modals/RemoveInstructionsModal";
21+
import Prism from "prismjs";
22+
import "prismjs/components/prism-python";
2123

2224
const InstructionsPanel = () => {
2325
const [showModal, setShowModal] = useState(false);
@@ -50,11 +52,15 @@ const InstructionsPanel = () => {
5052

5153
const applySyntaxHighlighting = (container) => {
5254
const codeElements = container.querySelectorAll(
53-
".language-python, .language-html, .language-css",
55+
".language-python, .language-html, .language-css, .language-javascript",
5456
);
5557

5658
codeElements.forEach((element) => {
57-
window.Prism.highlightElement(element);
59+
if (window.syntaxHighlight) {
60+
window.syntaxHighlight.highlightElement(element);
61+
} else {
62+
Prism.highlightElement(element);
63+
}
5864
});
5965
};
6066

src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.test.js

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import configureStore from "redux-mock-store";
55
import { setProjectInstructions } from "../../../../redux/EditorSlice";
66
import { act } from "react";
77
import Modal from "react-modal";
8+
import Prism from "prismjs";
89

910
window.HTMLElement.prototype.scrollTo = jest.fn();
10-
window.Prism = {
11+
jest.mock("prismjs", () => ({
12+
...jest.requireActual("prismjs"),
1113
highlightElement: jest.fn(),
12-
};
14+
}));
1315

1416
describe("When instructionsEditable is true", () => {
1517
describe("When there are instructions", () => {
@@ -222,6 +224,7 @@ describe("When instructions are not editable", () => {
222224
<code class='language-python'>print('hello')</code>
223225
<code class='language-html'><p>Hello world</p></code>
224226
<code class='language-css'>.hello { color: purple }</code>
227+
<code class='language-javascript'>const element = document.getElementById("my-element")</code>
225228
`,
226229
},
227230
],
@@ -262,17 +265,67 @@ describe("When instructions are not editable", () => {
262265

263266
test("Applies syntax highlighting to python code", () => {
264267
const codeElement = document.getElementsByClassName("language-python")[0];
265-
expect(window.Prism.highlightElement).toHaveBeenCalledWith(codeElement);
268+
expect(Prism.highlightElement).toHaveBeenCalledWith(codeElement);
266269
});
267270

268271
test("Applies syntax highlighting to HTML code", () => {
269272
const codeElement = document.getElementsByClassName("language-html")[0];
270-
expect(window.Prism.highlightElement).toHaveBeenCalledWith(codeElement);
273+
expect(Prism.highlightElement).toHaveBeenCalledWith(codeElement);
271274
});
272275

273276
test("Applies syntax highlighting to CSS code", () => {
274277
const codeElement = document.getElementsByClassName("language-css")[0];
275-
expect(window.Prism.highlightElement).toHaveBeenCalledWith(codeElement);
278+
expect(Prism.highlightElement).toHaveBeenCalledWith(codeElement);
279+
});
280+
281+
test("Applies syntax highlighting to javascript code", () => {
282+
const codeElement = document.getElementsByClassName(
283+
"language-javascript",
284+
)[0];
285+
expect(Prism.highlightElement).toHaveBeenCalledWith(codeElement);
286+
});
287+
});
288+
289+
describe("When window.syntaxHighlight is defined", () => {
290+
beforeEach(() => {
291+
window.syntaxHighlight = {
292+
highlightElement: jest.fn(),
293+
};
294+
const mockStore = configureStore([]);
295+
const initialState = {
296+
editor: {
297+
project: {},
298+
instructionsEditable: false,
299+
},
300+
instructions: {
301+
project: {
302+
steps: [
303+
{
304+
content: "<code class='language-python'>print('hello')</code>",
305+
},
306+
],
307+
},
308+
quiz: {},
309+
currentStepPosition: 0,
310+
},
311+
};
312+
const store = mockStore(initialState);
313+
render(
314+
<Provider store={store}>
315+
<InstructionsPanel />
316+
</Provider>,
317+
);
318+
});
319+
320+
test("Applies syntax highlighting using window.syntaxHighlight", () => {
321+
const codeElement = document.getElementsByClassName("language-python")[0];
322+
expect(window.syntaxHighlight.highlightElement).toHaveBeenCalledWith(
323+
codeElement,
324+
);
325+
});
326+
327+
afterEach(() => {
328+
delete window.syntaxHighlight;
276329
});
277330
});
278331

@@ -354,7 +407,7 @@ describe("When instructions are not editable", () => {
354407

355408
test("Applies syntax highlighting", () => {
356409
const codeElement = document.getElementsByClassName("language-python")[0];
357-
expect(window.Prism.highlightElement).toHaveBeenCalledWith(codeElement);
410+
expect(Prism.highlightElement).toHaveBeenCalledWith(codeElement);
358411
});
359412

360413
test("Fires a quizIsReady event", () => {

src/containers/WebComponentLoader.jsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,24 @@ const WebComponentLoader = (props) => {
161161
dispatch(setReadOnly(readOnly));
162162
}, [readOnly, dispatch]);
163163

164+
useEffect(() => {
165+
// Create a script element to save the existing Prism object if there is one
166+
const script = document.createElement("script");
167+
script.textContent = `
168+
if (window.Prism) {
169+
window.syntaxHighlight = window.Prism;
170+
}
171+
`;
172+
173+
// Append the script to the document body
174+
document.body.appendChild(script);
175+
176+
// Clean up the script when the component unmounts
177+
return () => {
178+
document.body.removeChild(script);
179+
};
180+
}, []);
181+
164182
const renderSuccessState = () => (
165183
<>
166184
<SettingsContext.Provider

src/containers/WebComponentLoader.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const user = { access_token: "my_token" };
4040
describe("When initially rendered", () => {
4141
beforeEach(() => {
4242
document.dispatchEvent = jest.fn();
43+
window.Prism = jest.fn();
4344
const middlewares = [localStorageUserMiddleware(setUser)];
4445
const mockStore = configureStore(middlewares);
4546
const initialState = {
@@ -85,6 +86,10 @@ describe("When initially rendered", () => {
8586
);
8687
});
8788

89+
test("It saves window.Prism to window.syntaxHighlight", () => {
90+
expect(window.syntaxHighlight).toEqual(window.Prism);
91+
});
92+
8893
describe("react app API endpoint", () => {
8994
describe("when react app API endpoint isn't set", () => {
9095
beforeEach(() => {

0 commit comments

Comments
 (0)