Skip to content

Commit f7872b7

Browse files
committed
Wrap copydeck placeholders in HTML at render time
Wrap copydeck placeholders in semantic HTML spans at render time Placeholder values (name, loc, codeLine) are now HTML-escaped and wrapped in typed spans (pfem__var, pfem__line/pfem__file, pfem__code) when building the HTML output. Removes all inline styling / <i> tags.
1 parent e2c90fb commit f7872b7

4 files changed

Lines changed: 79 additions & 14 deletions

File tree

copydecks/en/copydeck.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
"not_message": ["is not defined"]
2424
},
2525
"title": "This variable doesn't exist yet",
26-
"summary": "Your code uses the variable \"{{name}}\", but it hasn't been created yet. Check {{loc}}. If you meant to print the text <i>{{name}}</i>, put it in double quotes.",
27-
"why": "Without speech marks Python treats <i>{{name}}</i> as a variable, and this variable does not exist yet.",
26+
"summary": "Your code uses the variable {{name}}, but it hasn't been created yet. Check {{loc}}. If you meant to print the text {{name}}, put it in double quotes.",
27+
"why": "Without speech marks Python treats {{name}} as a variable, and this variable does not exist yet.",
2828
"steps": [
2929
"If it is meant to be text, put speech marks around {{name}}.",
3030
"If it is meant to be a variable, make it first (for example: {{name}} = 0).",
@@ -36,7 +36,7 @@
3636
"match_message": ["is not defined"]
3737
},
3838
"title": "This variable doesn't exist here",
39-
"summary": "\"{{name}}\" might be created somewhere else, but you're using it at {{loc}}. If you meant the text <i>{{name}}</i>, put it in double quotes.",
39+
"summary": "{{name}} might be created somewhere else, but you're using it at {{loc}}. If you meant the text {{name}}, put it in double quotes.",
4040
"why": "A variable created in another place might not be available here.",
4141
"steps": [
4242
"Move the line that makes it to above where you use it.",
@@ -50,7 +50,7 @@
5050
"variants": [
5151
{
5252
"title": "Variable used before it gets a value in this part of the code",
53-
"summary": "Here, \"{{name}}\" is used at {{loc}} before you give it a value.",
53+
"summary": "Here, {{name}} is used at {{loc}} before you give it a value.",
5454
"why": "You have used the variable before it has been given a value. If used within a subroutine, the variable must either be global and given a value outside the subroutine definition, or local and given a value inside the subroutine, before it is used. ",
5555
"steps": [
5656
"Give it a value first (add a line like {{name}} = ... before you use it)."

src/engine.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,32 @@ const pickVariant = (trace: Trace, code: string | undefined) => {
7070
codeLine: codeLine
7171
};
7272

73+
const linePart = trace.line ? `<span class="pfem__line">${escapeHtml(lineStr)} ${escapeHtml(String(trace.line))}</span>` : null;
74+
const filePart = trace.file ? `<span class="pfem__file">${escapeHtml(trace.file)}</span>` : null;
75+
const htmlLoc =
76+
linePart && filePart ? `${linePart} ${escapeHtml(inStr)} ${filePart}` :
77+
linePart ?? filePart ?? escapeHtml(thisFileStr);
78+
79+
const htmlTransforms: Record<string, (v: string) => string> = {
80+
name: (v) => `<span class="pfem__var">${escapeHtml(v)}</span>`,
81+
loc: (_) => htmlLoc,
82+
codeLine: (v) => `<span class="pfem__code">${escapeHtml(v)}</span>`,
83+
};
84+
7385
for (let i = 0; i < entry.variants.length; i++) {
7486
const v = entry.variants[i];
7587
if (!matches(v)) continue;
7688

77-
const title = tmpl(v.title, vars);
89+
const title = tmpl(v.title, vars);
7890
const summary = tmpl(v.summary, vars);
79-
const why = v.why ? tmpl(v.why, vars) : undefined;
80-
const steps = v.steps?.map((s) => tmpl(s, vars));
81-
const badges = v.badges;
91+
const why = v.why ? tmpl(v.why, vars) : undefined;
92+
const steps = v.steps?.map((s) => tmpl(s, vars));
93+
const badges = v.badges;
94+
95+
const titleHtml = tmpl(v.title, vars, htmlTransforms);
96+
const summaryHtml = tmpl(v.summary, vars, htmlTransforms);
97+
const whyHtml = v.why ? tmpl(v.why, vars, htmlTransforms) : undefined;
98+
const stepsHtml = v.steps?.map((s) => tmpl(s, vars, htmlTransforms));
8299

83100
let patch: string | undefined = undefined;
84101
if (trace.type === "AttributeError" && /\.push\s*\(/i.test(codeLine)) {
@@ -95,10 +112,10 @@ const pickVariant = (trace: Trace, code: string | undefined) => {
95112
}
96113

97114
const html = [
98-
`<div class="pfem__title">${escapeHtml(title)}</div>`,
99-
`<div class="pfem__summary">${summary}</div>`,
100-
why ? `<div class="pfem__why">${why}</div>` : "",
101-
steps?.length ? `<ul class="pfem__steps">${steps.map((s) => `<li>${s}</li>`).join("")}</ul>` : "",
115+
`<div class="pfem__title">${titleHtml}</div>`,
116+
`<div class="pfem__summary">${summaryHtml}</div>`,
117+
whyHtml ? `<div class="pfem__why">${whyHtml}</div>` : "",
118+
stepsHtml?.length ? `<ul class="pfem__steps">${stepsHtml.map((s) => `<li>${s}</li>`).join("")}</ul>` : "",
102119
patch ? `<pre class="pfem__patch">${escapeHtml(patch)}</pre>` : "",
103120
`<details class="pfem__details"><summary>${escapeHtml(getUiString("originalError", "Original error"))}</summary><pre>${escapeHtml(
104121
(trace.type || getUiString("error", "Error")) + ": " + trace.message

src/utils.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
export const escapeHtml = (s: string) =>
22
s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]!));
33

4-
export const tmpl = (s: string, vars: Record<string, string>) =>
5-
(s || "").replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, k) => (k in vars ? String(vars[k]) : ""));
4+
export const tmpl = (
5+
s: string,
6+
vars: Record<string, string>,
7+
transforms?: Record<string, (v: string) => string>
8+
) =>
9+
(s || "").replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, k) => {
10+
if (!(k in vars)) return "";
11+
const v = String(vars[k]);
12+
return transforms?.[k] ? transforms[k](v) : v;
13+
});
614

715
export const safeRegexTest = (pattern: string, input: string) => {
816
try {

tests/engine.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,44 @@ AttributeError: 'list' object has no attribute 'push'`;
107107
expect(res.trace.type).toBe("AttributeError");
108108
expect(res.patch).toContain(".append(");
109109
});
110+
111+
it("plain result fields contain no HTML tags", () => {
112+
const code = `print("Hello")\nprint(kittens)\n`;
113+
const raw = `Traceback (most recent call last):
114+
File "main.py", line 2, in <module>
115+
NameError: name 'kittens' is not defined`;
116+
const res = friendlyExplain({ error: raw, code, runtime: "skulpt" });
117+
expect(res.summary).not.toMatch(/<[^>]+>/);
118+
expect(res.why).not.toMatch(/<[^>]+>/);
119+
res.steps?.forEach((s) => expect(s).not.toMatch(/<[^>]+>/));
120+
});
121+
122+
it("html output wraps {{name}} in pfem__var span", () => {
123+
const code = `print("Hello")\nprint(kittens)\n`;
124+
const raw = `Traceback (most recent call last):
125+
File "main.py", line 2, in <module>
126+
NameError: name 'kittens' is not defined`;
127+
const res = friendlyExplain({ error: raw, code, runtime: "skulpt" });
128+
expect(res.html).toContain('<span class="pfem__var">kittens</span>');
129+
});
130+
131+
it("html output wraps {{loc}} line and file in separate spans", () => {
132+
const code = `print("Hello")\nprint(kittens)\n`;
133+
const raw = `Traceback (most recent call last):
134+
File "main.py", line 2, in <module>
135+
NameError: name 'kittens' is not defined`;
136+
const res = friendlyExplain({ error: raw, code, runtime: "skulpt" });
137+
expect(res.html).toContain('<span class="pfem__line">');
138+
expect(res.html).toContain('<span class="pfem__file">main.py</span>');
139+
});
140+
141+
it("escapes HTML in codeLine within html output", () => {
142+
const code = `for i in range(3)<script>alert(1)</script>\n print(i)`;
143+
const raw = `Traceback (most recent call last):
144+
File "main.py", line 1
145+
SyntaxError: invalid syntax`;
146+
const res = friendlyExplain({ error: raw, code, runtime: "skulpt" });
147+
expect(res.html).not.toContain("<script>");
148+
expect(res.html).toContain("&lt;script&gt;");
149+
});
110150
});

0 commit comments

Comments
 (0)