diff --git a/eslint.config.ts b/eslint.config.ts
index 35e26ed111d..50c1bcecd7f 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -311,6 +311,7 @@ export default defineConfig([
'func-names': [0],
'func-style': [0],
'getter-return': [2],
+ 'gitea/unescaped-html-literal': [2],
'grouped-accessor-pairs': [2],
'guard-for-in': [0],
'id-denylist': [0],
@@ -1239,6 +1240,9 @@ export default defineConfig([
{
files: ['*', 'tools/**/*', 'tests/**/*'],
languageOptions: {globals: globals.nodeBuiltin},
+ rules: {
+ 'gitea/unescaped-html-literal': [0],
+ },
},
{
files: ['web_src/**/*'],
diff --git a/tools/eslint-rules/unescaped-html-literal.test.ts b/tools/eslint-rules/unescaped-html-literal.test.ts
index af3d473e764..636279bbe94 100644
--- a/tools/eslint-rules/unescaped-html-literal.test.ts
+++ b/tools/eslint-rules/unescaped-html-literal.test.ts
@@ -28,6 +28,10 @@ ruleTester.run('unescaped-html-literal', rule, {
code: 'const helloTemplate = (name) => html`
Hello ${name}!
`;',
languageOptions: {ecmaVersion: 2017},
},
+ {
+ code: 'const attr = htmlRaw` width="${width}"`, img = htmlRaw`
`, cmp = "a < b";',
+ languageOptions: {ecmaVersion: 2017},
+ },
],
invalid: [
{
@@ -66,6 +70,21 @@ ruleTester.run('unescaped-html-literal', rule, {
},
],
},
+ {
+ code: "const rowHTML = `${cells}`, tailHTML = '', textHTML = 'Hello World'",
+ languageOptions: {ecmaVersion: 2017},
+ errors: [
+ {
+ message: 'Unescaped HTML literal. Use html`` tag template literal for secure escaping.',
+ },
+ {
+ message: 'Unescaped HTML literal. Use html`` tag template literal for secure escaping.',
+ },
+ {
+ message: 'Unescaped HTML literal. Use html`` tag template literal for secure escaping.',
+ },
+ ],
+ },
{
code: 'const helloHTML = foo`Hello ${name}!
`',
languageOptions: {ecmaVersion: 2017},
diff --git a/tools/eslint-rules/unescaped-html-literal.ts b/tools/eslint-rules/unescaped-html-literal.ts
index 87c40f0518e..3ae51a97ecc 100644
--- a/tools/eslint-rules/unescaped-html-literal.ts
+++ b/tools/eslint-rules/unescaped-html-literal.ts
@@ -2,7 +2,8 @@
// https://github.com/github/eslint-plugin-github/blob/main/lib/rules/unescaped-html-literal.js
import type {JSRuleDefinition, JSRuleDefinitionTypeOptions} from 'eslint';
-const htmlOpenTag = /^\s*<[a-zA-Z]/;
+const htmlTag = /<\/?[a-z][\w-]*[\s/>]/i;
+const escapingTags = new Set(['html', 'htmlRaw']);
const rule: JSRuleDefinition = {
meta: {
@@ -14,7 +15,7 @@ const rule: JSRuleDefinition = {
create: (context) => ({
Literal(node) {
- if (typeof node.value !== 'string' || !htmlOpenTag.test(node.value)) return;
+ if (typeof node.value !== 'string' || !htmlTag.test(node.value)) return;
context.report({
node,
@@ -22,11 +23,10 @@ const rule: JSRuleDefinition = {
});
},
TemplateLiteral(node) {
- const templateStart = node.quasis[0]?.value.raw;
- if (!templateStart || !htmlOpenTag.test(templateStart)) return;
+ if (node.quasis.every((quasi) => !htmlTag.test(quasi.value.raw))) return;
const parent = node.parent;
- if (parent?.type === 'TaggedTemplateExpression' && parent.tag.type === 'Identifier' && parent.tag.name === 'html') return;
+ if (parent?.type === 'TaggedTemplateExpression' && parent.tag.type === 'Identifier' && escapingTags.has(parent.tag.name)) return;
context.report({
node,
diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue
index 48d92229314..067fa15d4dd 100644
--- a/web_src/js/components/ActivityHeatmap.vue
+++ b/web_src/js/components/ActivityHeatmap.vue
@@ -3,6 +3,7 @@ import {computed, onBeforeUnmount, onMounted} from 'vue';
import tippy, {createSingleton} from 'tippy.js';
import type {CreateSingletonInstance, Instance} from 'tippy.js';
import {getCurrentLocale} from '../utils.ts';
+import {html} from '../utils/html.ts';
type HeatmapValue = {date: Date; count: number};
type HeatmapCell = {date: Date; colorIndex: number; ariaLabel: string; tooltip: string};
@@ -96,7 +97,7 @@ const grid = computed(() => {
date: new Date(cursor),
colorIndex: hit ? hit.colorIndex : 0,
ariaLabel: `${head} ${on} ${dateStr}`,
- tooltip: `${head} ${on} ${dateStr}`,
+ tooltip: html`${head} ${on} ${dateStr}`,
});
cursor.setDate(cursor.getDate() + 1);
}
diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts
index d81fb1c7ff0..efcf574ff8b 100644
--- a/web_src/js/markup/mermaid.ts
+++ b/web_src/js/markup/mermaid.ts
@@ -110,7 +110,7 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promisebody { margin: 0; overflow: hidden; } #mermaid { display: block; margin: 0 auto; }`;
const viewBoxHeight = svgNode.viewBox.baseVal?.height;
if (viewBoxHeight) iframe.style.height = `${Math.max(Math.ceil(viewBoxHeight), 85)}px`; // min-height keeps the buttons from overlapping
diff --git a/web_src/js/modules/devtest.ts b/web_src/js/modules/devtest.ts
index 8efe7e99596..f4f225c4037 100644
--- a/web_src/js/modules/devtest.ts
+++ b/web_src/js/modules/devtest.ts
@@ -85,7 +85,7 @@ function initDevtestAnsiRender(container: HTMLElement) {
...withSource(`first${esc}[Ksecond${esc}[2Jthird`),
...withSource(`cursor ${esc}[3Amovement, private ${esc}[?25lCSI, ${esc}]0;title${esc}\\titles, ${esc}Pquery${esc}\\strings, truncated${esc}[38;5;`),
...withSource('Reading... 10%\b\b\b100%'),
- ...withSource(' & "quotes", and a bare url https://example.com'),
+ ...withSource(' & "quotes", and a bare url https://example.com'), // eslint-disable-line gitea/unescaped-html-literal -- plain text that the console must escape
' ',
`${esc}[31man unterminated color`,
'carries into the following lines',
diff --git a/web_src/js/modules/shortcut.ts b/web_src/js/modules/shortcut.ts
index 58d2ac26a4a..44622228b1e 100644
--- a/web_src/js/modules/shortcut.ts
+++ b/web_src/js/modules/shortcut.ts
@@ -108,7 +108,7 @@ function showShortcutHelp() {
seenShortcuts.add(shortcutIdentifier);
let keysHtml = '';
for (const keys of shortcutKeys.split(' ')) {
- keysHtml += `${keys} `;
+ keysHtml += html`${keys} `;
}
list.append(createElementFromHTML(html`| ${htmlRaw(keysHtml)} | ${label} |
`));
}
diff --git a/web_src/js/modules/tippy.ts b/web_src/js/modules/tippy.ts
index 531cfa9ceb5..68865bbfbd5 100644
--- a/web_src/js/modules/tippy.ts
+++ b/web_src/js/modules/tippy.ts
@@ -166,8 +166,7 @@ function attachTooltip(target: Element, content: Content | null = null): Instanc
let allowHTML = target.getAttribute('data-tooltip-render') === 'html';
if (!allowHTML && typeof content === 'string') {
- content = htmlEscape(content);
- content = content.replace(/\n/g, '
');
+ content = htmlEscape(content).split('\n').join(html`
`);
allowHTML = true;
}