Initial commit
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
ba3d2c88e8
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "static",
|
||||||
|
"runtimeExecutable": "python",
|
||||||
|
"runtimeArgs": ["-m", "http.server", "8934"],
|
||||||
|
"port": 8934
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Baut aus src/alt-text-checker.js ein einzeiliges "javascript:"-Bookmarklet
|
||||||
|
// und schreibt es nach dist/bookmarklet.txt sowie in INSTALLIEREN.html.
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { minify } = require('terser');
|
||||||
|
|
||||||
|
const SRC = path.join(__dirname, 'src', 'alt-text-checker.js');
|
||||||
|
const DIST_DIR = path.join(__dirname, 'dist');
|
||||||
|
const HTML_FILE = path.join(__dirname, 'INSTALLIEREN.html');
|
||||||
|
|
||||||
|
async function build() {
|
||||||
|
const source = fs.readFileSync(SRC, 'utf8');
|
||||||
|
|
||||||
|
const result = await minify(source, {
|
||||||
|
compress: { unused: true, dead_code: true },
|
||||||
|
mangle: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
|
||||||
|
const bookmarklet = 'javascript:' + encodeURIComponent(result.code);
|
||||||
|
|
||||||
|
fs.mkdirSync(DIST_DIR, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(DIST_DIR, 'alt-text-checker.min.js'), result.code, 'utf8');
|
||||||
|
fs.writeFileSync(path.join(DIST_DIR, 'bookmarklet.txt'), bookmarklet, 'utf8');
|
||||||
|
|
||||||
|
console.log(`Minified: ${source.length} -> ${result.code.length} bytes`);
|
||||||
|
console.log(`Bookmarklet: ${bookmarklet.length} bytes`);
|
||||||
|
|
||||||
|
if (fs.existsSync(HTML_FILE)) {
|
||||||
|
let html = fs.readFileSync(HTML_FILE, 'utf8');
|
||||||
|
const before = html;
|
||||||
|
html = html.replace(
|
||||||
|
/(<a id="bookmarklet-link"[^>]*href=")[^"]*(")/,
|
||||||
|
`$1${bookmarklet.replace(/&/g, '&').replace(/"/g, '"')}$2`
|
||||||
|
);
|
||||||
|
if (html !== before) {
|
||||||
|
fs.writeFileSync(HTML_FILE, html, 'utf8');
|
||||||
|
console.log('INSTALLIEREN.html aktualisiert.');
|
||||||
|
} else {
|
||||||
|
console.log('Hinweis: Platzhalter <a id="bookmarklet-link"> nicht gefunden, INSTALLIEREN.html nicht verändert.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"name": "alttextchecker",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "node build.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,372 @@
|
||||||
|
/**
|
||||||
|
* Alt-Text Checker – Bookmarklet
|
||||||
|
* Prüft Bilder auf einer Webseite auf Barrierefreiheit (Alt-Text, WCAG 2.1).
|
||||||
|
* Läuft komplett lokal im Browser, keine Datenübertragung.
|
||||||
|
*
|
||||||
|
* Sicherheitshinweis: Attributwerte von der besuchten Seite (alt, title, IDs, …)
|
||||||
|
* gelten als nicht vertrauenswürdig. Sie werden daher ausschließlich über
|
||||||
|
* textContent / DOM-Knoten eingefügt, niemals über innerHTML mit String-Konkatenation.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const NS = 'atc'; // Namespace für CSS-Klassen
|
||||||
|
const STORAGE_PREFIX = 'alt-text-checker:excluded:';
|
||||||
|
|
||||||
|
const COLORS = {
|
||||||
|
success: { bg: 'lime', outline: '#000', text: '#000' },
|
||||||
|
error: { bg: '#EB0000', outline: '#fff', text: '#fff' },
|
||||||
|
warning: { bg: 'orange', outline: '#000', text: '#000' },
|
||||||
|
excluded:{ bg: '#e0e0e0', outline: '#666', text: '#333' },
|
||||||
|
info: { bg: 'yellow', outline: '#000', text: '#000' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Immer sichtbarer Text neben dem Icon – Icons allein sind für Screenreader
|
||||||
|
// nicht eindeutig (z.B. wird ✔ als "Häkchen" statt "OK" vorgelesen).
|
||||||
|
const BADGE_TEXT = {
|
||||||
|
success: 'OK',
|
||||||
|
error: 'FEHLER',
|
||||||
|
warning: 'WARNUNG',
|
||||||
|
excluded: 'AUSGESCHLOSSEN',
|
||||||
|
info: 'INFO'
|
||||||
|
};
|
||||||
|
|
||||||
|
const BASE_LABEL_STYLE = {
|
||||||
|
'font-family': 'sans-serif',
|
||||||
|
'font-size': 'small',
|
||||||
|
padding: '5px 8px',
|
||||||
|
outline: '2px dotted #000',
|
||||||
|
'outline-offset': '-3px',
|
||||||
|
border: '0',
|
||||||
|
cursor: 'default',
|
||||||
|
'z-index': '2147483647'
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Speicherung der Ausschlüsse (pro Seiten-URL, dauerhaft im localStorage)
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
function storageKey() {
|
||||||
|
return STORAGE_PREFIX + location.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadExclusions() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(storageKey());
|
||||||
|
return raw ? JSON.parse(raw) : [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveExclusions(list) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(storageKey(), JSON.stringify(list));
|
||||||
|
} catch (e) {
|
||||||
|
// localStorage evtl. nicht verfügbar (z.B. privater Modus) – ignorieren
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function imageIdentifier(img) {
|
||||||
|
// src ist die stabilste Kennung eines Bildes über mehrere Seitenaufrufe hinweg
|
||||||
|
return img.currentSrc || img.src || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExcluded(img, exclusions) {
|
||||||
|
const id = imageIdentifier(img);
|
||||||
|
return id !== null && exclusions.includes(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleExclusion(img) {
|
||||||
|
const id = imageIdentifier(img);
|
||||||
|
if (id === null) return;
|
||||||
|
const exclusions = loadExclusions();
|
||||||
|
const idx = exclusions.indexOf(id);
|
||||||
|
if (idx === -1) exclusions.push(id);
|
||||||
|
else exclusions.splice(idx, 1);
|
||||||
|
saveExclusions(exclusions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Hilfsfunktionen
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Durchsucht auch offene Shadow-DOM-Bäume
|
||||||
|
function deepQueryAll(selector, root, callback) {
|
||||||
|
let count = 0;
|
||||||
|
Array.from((root || document).querySelectorAll(selector)).forEach((el) => {
|
||||||
|
callback(el);
|
||||||
|
count += 1;
|
||||||
|
});
|
||||||
|
Array.from((root || document).querySelectorAll('*')).forEach((el) => {
|
||||||
|
if (el.shadowRoot) count += deepQueryAll(selector, el.shadowRoot, callback);
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleToString(styleObj) {
|
||||||
|
return Object.keys(styleObj)
|
||||||
|
.map((key) => `${key}:${styleObj[key]}`)
|
||||||
|
.join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInsideLink(el) {
|
||||||
|
return !!(el.parentNode && (el.parentNode.tagName === 'A' || isInsideLink(el.parentNode)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePreviousRun() {
|
||||||
|
deepQueryAll(`.${NS}`, document, (el) => el.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Baut sicher einen DOM-Fragment aus Text- und Markup-Segmenten auf.
|
||||||
|
// Strings = reiner (von uns erzeugter) Text. {code:} / {strike:} = Wert wird
|
||||||
|
// per textContent in <code>/<s> gesetzt – niemals per innerHTML.
|
||||||
|
function buildFragment(segments) {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
segments.forEach((seg) => {
|
||||||
|
if (typeof seg === 'string') {
|
||||||
|
frag.appendChild(document.createTextNode(seg));
|
||||||
|
} else if (seg.code !== undefined) {
|
||||||
|
const code = document.createElement('code');
|
||||||
|
code.textContent = seg.code;
|
||||||
|
frag.appendChild(code);
|
||||||
|
} else if (seg.strike !== undefined) {
|
||||||
|
const s = document.createElement('s');
|
||||||
|
s.textContent = seg.strike;
|
||||||
|
frag.appendChild(s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Label-Erstellung (Badge neben dem geprüften Element)
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
// content: bei opts.global ein reiner String (von uns generiert),
|
||||||
|
// sonst ein Array von Segmenten für buildFragment().
|
||||||
|
function createLabel(el, kind, content, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
const colors = COLORS[kind];
|
||||||
|
const isGlobal = !!opts.global;
|
||||||
|
|
||||||
|
const wrapper = document.createElement(isGlobal ? 'label' : 'button');
|
||||||
|
wrapper.className = `${NS} ${NS}-${kind}`.trim();
|
||||||
|
|
||||||
|
const style = Object.assign({}, BASE_LABEL_STYLE, {
|
||||||
|
'background-color': colors.bg,
|
||||||
|
'outline-color': colors.outline,
|
||||||
|
color: colors.text
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.position === 'absolute' && el.offsetParent && el.offsetParent !== document.body) {
|
||||||
|
style.left = `${el.offsetLeft}px`;
|
||||||
|
style.top = `${el.offsetTop}px`;
|
||||||
|
}
|
||||||
|
if (opts.extraStyle) Object.assign(style, opts.extraStyle);
|
||||||
|
|
||||||
|
wrapper.setAttribute('style', styleToString(style));
|
||||||
|
|
||||||
|
const icon = opts.icon || '';
|
||||||
|
if (isGlobal) {
|
||||||
|
wrapper.textContent = icon ? `${icon} ${content}` : content;
|
||||||
|
} else {
|
||||||
|
wrapper.appendChild(document.createTextNode(icon));
|
||||||
|
|
||||||
|
const badge = document.createElement('span');
|
||||||
|
badge.className = `${NS}-badge`;
|
||||||
|
badge.textContent = BADGE_TEXT[kind] || '';
|
||||||
|
badge.style.marginLeft = '4px';
|
||||||
|
badge.style.fontWeight = '700';
|
||||||
|
wrapper.appendChild(badge);
|
||||||
|
|
||||||
|
const details = document.createElement('span');
|
||||||
|
details.className = `${NS}-details`;
|
||||||
|
details.appendChild(buildFragment(content));
|
||||||
|
details.style.color = 'inherit';
|
||||||
|
details.style.marginLeft = '4px';
|
||||||
|
wrapper.appendChild(details);
|
||||||
|
|
||||||
|
if (!opts.alwaysOpen) {
|
||||||
|
details.style.display = 'none';
|
||||||
|
const show = () => {
|
||||||
|
if (document.querySelectorAll(`.${NS}-toggle:checked`).length === 0) {
|
||||||
|
details.style.display = 'inline';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const hide = () => {
|
||||||
|
if (document.querySelectorAll(`.${NS}-toggle:checked`).length === 0) {
|
||||||
|
details.style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
wrapper.addEventListener('focus', show);
|
||||||
|
wrapper.addEventListener('mouseover', show);
|
||||||
|
wrapper.addEventListener('blur', hide);
|
||||||
|
wrapper.addEventListener('mouseout', hide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.onClick) {
|
||||||
|
wrapper.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
opts.onClick();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGlobal && opts.withToggle) {
|
||||||
|
wrapper.appendChild(document.createTextNode(' — alle Details anzeigen'));
|
||||||
|
const checkbox = document.createElement('input');
|
||||||
|
checkbox.type = 'checkbox';
|
||||||
|
checkbox.className = `${NS}-toggle`;
|
||||||
|
checkbox.style.margin = '0 0 0 4px';
|
||||||
|
checkbox.style.verticalAlign = 'baseline';
|
||||||
|
checkbox.addEventListener('change', function () {
|
||||||
|
document.querySelectorAll(`.${NS}-details`).forEach((d) => {
|
||||||
|
d.style.display = this.checked ? 'inline' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
wrapper.appendChild(checkbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGlobal) {
|
||||||
|
document.querySelectorAll(`.${NS}-global`).forEach((n) => n.remove());
|
||||||
|
wrapper.className += ` ${NS}-global`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.position === 'absolute' && el.parentNode) {
|
||||||
|
el.parentNode.insertBefore(wrapper, el.nextSibling);
|
||||||
|
} else {
|
||||||
|
(opts.insertInto || el.parentNode).appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Prüf-Logik: referenzierten Text auflösen (z.B. aria-describedby)
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Gibt den (zusammengefügten) sichtbaren Text der referenzierten Elemente
|
||||||
|
// zurück, oder null falls das Attribut fehlt / nichts gefunden wurde.
|
||||||
|
// Markiert die referenzierten Elemente zusätzlich selbst auf der Seite.
|
||||||
|
function resolveIdRefs(attrName, imgEl) {
|
||||||
|
const value = imgEl.getAttribute(attrName);
|
||||||
|
if (!value) return null;
|
||||||
|
|
||||||
|
const ids = value.split(' ').map((s) => s.trim()).filter(Boolean);
|
||||||
|
const texts = [];
|
||||||
|
|
||||||
|
ids.forEach((id) => {
|
||||||
|
const ref = document.getElementById(id);
|
||||||
|
if (!ref) return;
|
||||||
|
createLabel(
|
||||||
|
ref,
|
||||||
|
'warning',
|
||||||
|
[`Referenziert von [${attrName}="`, { code: imgEl.id || '…' }, '"]'],
|
||||||
|
{ icon: '⚠️', position: 'absolute' }
|
||||||
|
);
|
||||||
|
const refText = ref.textContent.trim();
|
||||||
|
if (refText) texts.push(refText);
|
||||||
|
});
|
||||||
|
|
||||||
|
return texts.length ? texts.join(' ') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Hauptanalyse
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
removePreviousRun();
|
||||||
|
const exclusions = loadExclusions();
|
||||||
|
const stats = { ok: 0, error: 0, warning: 0, excluded: 0, total: 0 };
|
||||||
|
|
||||||
|
// Fehlerhafte Verwendung von alt auf Nicht-<img>-Elementen
|
||||||
|
deepQueryAll('a[alt], button[alt], label[alt]', document, (el) => {
|
||||||
|
createLabel(
|
||||||
|
el,
|
||||||
|
'error',
|
||||||
|
[`<${el.tagName.toLowerCase()}> mit ungültigem `, { code: `alt="${el.getAttribute('alt')}"` }],
|
||||||
|
{ icon: '✖', position: 'absolute' }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
deepQueryAll('img, [role="img"]', document, (img) => {
|
||||||
|
stats.total += 1;
|
||||||
|
|
||||||
|
if (isExcluded(img, exclusions)) {
|
||||||
|
stats.excluded += 1;
|
||||||
|
createLabel(img, 'excluded', ['Ausgeschlossen (manuell als dekorativ markiert)'], {
|
||||||
|
icon: '⦸',
|
||||||
|
position: 'absolute',
|
||||||
|
onClick: () => {
|
||||||
|
toggleExclusion(img);
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAlt = img.hasAttribute('alt');
|
||||||
|
const altValue = hasAlt ? img.getAttribute('alt') : null;
|
||||||
|
const hasAriaLabel = img.hasAttribute('aria-label');
|
||||||
|
const describedBy = resolveIdRefs('aria-describedby', img);
|
||||||
|
const labelledBy = resolveIdRefs('aria-labelledby', img);
|
||||||
|
const hasTitle = img.hasAttribute('title');
|
||||||
|
const hasLongdesc = img.hasAttribute('longdesc');
|
||||||
|
const hasAccessibleName = hasAriaLabel || describedBy !== null || labelledBy !== null;
|
||||||
|
const linkPrefix = isInsideLink(img) ? 'Bild in Link: ' : '';
|
||||||
|
|
||||||
|
const extras = [];
|
||||||
|
if (hasTitle) extras.push(' ', { code: `title="${img.getAttribute('title')}"` });
|
||||||
|
if (hasLongdesc) extras.push(' ', { code: `longdesc="${img.getAttribute('longdesc')}"` });
|
||||||
|
|
||||||
|
let segments;
|
||||||
|
let kind;
|
||||||
|
|
||||||
|
if (hasAlt) {
|
||||||
|
const isDecorative = altValue.trim().length === 0;
|
||||||
|
segments = [linkPrefix, { code: `alt="${altValue}"` }, ...extras];
|
||||||
|
kind = isDecorative ? 'warning' : 'success';
|
||||||
|
} else if (hasAccessibleName) {
|
||||||
|
const resolvedName = hasAriaLabel ? img.getAttribute('aria-label') : (labelledBy || describedBy);
|
||||||
|
segments = [
|
||||||
|
hasTitle ? '' : linkPrefix,
|
||||||
|
{ strike: 'alt' },
|
||||||
|
' fehlt, aber ARIA-Name vorhanden: ',
|
||||||
|
{ code: resolvedName },
|
||||||
|
...extras
|
||||||
|
];
|
||||||
|
kind = 'warning';
|
||||||
|
} else {
|
||||||
|
segments = [linkPrefix, { strike: 'alt' }, ' fehlt', ...extras];
|
||||||
|
kind = 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
stats[kind === 'success' ? 'ok' : kind] += 1;
|
||||||
|
|
||||||
|
createLabel(img, kind, segments, {
|
||||||
|
icon: kind === 'success' ? '✔' : kind === 'error' ? '✖' : '⚠️',
|
||||||
|
position: 'absolute',
|
||||||
|
onClick: () => {
|
||||||
|
toggleExclusion(img);
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const summaryKind = stats.error > 0 ? 'error' : stats.warning > 0 ? 'warning' : 'success';
|
||||||
|
const summaryText = stats.total
|
||||||
|
? `Bilder: ${stats.total} · ✔ ${stats.ok} OK · ✖ ${stats.error} Fehler · ⚠️ ${stats.warning} Warnung · ⦸ ${stats.excluded} ausgeschlossen`
|
||||||
|
: 'Keine Bilder auf dieser Seite gefunden.';
|
||||||
|
|
||||||
|
createLabel(document.body, stats.total ? summaryKind : 'info', summaryText, {
|
||||||
|
global: true,
|
||||||
|
withToggle: !!stats.total,
|
||||||
|
extraStyle: { position: 'fixed', top: '0', left: '0' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
|
})();
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Testseite – Alt-Text Checker</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; padding: 20px; }
|
||||||
|
img { width: 100px; height: 80px; background: #ddd; display: block; margin: 10px 0; }
|
||||||
|
figure { margin: 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Testseite für Alt-Text Checker</h1>
|
||||||
|
|
||||||
|
<p>1. Bild ohne alt-Attribut (Fehler erwartet):</p>
|
||||||
|
<img src="https://via.placeholder.com/100x80/ff0000/ffffff?text=1">
|
||||||
|
|
||||||
|
<p>2. Bild mit leerem alt (Warnung, dekorativ):</p>
|
||||||
|
<img src="https://via.placeholder.com/100x80/ffa500/ffffff?text=2" alt="">
|
||||||
|
|
||||||
|
<p>3. Bild mit Alt-Text (OK):</p>
|
||||||
|
<img src="https://via.placeholder.com/100x80/00ff00/000000?text=3" alt="Ein grünes Testbild mit der Zahl drei">
|
||||||
|
|
||||||
|
<p>4. Bild mit aria-label statt alt (Warnung):</p>
|
||||||
|
<img src="https://via.placeholder.com/100x80/0000ff/ffffff?text=4" aria-label="Blaues Testbild via ARIA">
|
||||||
|
|
||||||
|
<p>5. Bild in einem Link:</p>
|
||||||
|
<a href="#test"><img src="https://via.placeholder.com/100x80/purple/ffffff?text=5"></a>
|
||||||
|
|
||||||
|
<p>6. Bild mit aria-describedby:</p>
|
||||||
|
<img src="https://via.placeholder.com/100x80/808080/ffffff?text=6" aria-describedby="desc6">
|
||||||
|
<p id="desc6">Diese Beschreibung wird per aria-describedby referenziert.</p>
|
||||||
|
|
||||||
|
<p>7. Falsche Verwendung: alt auf einem Link statt auf dem Bild:</p>
|
||||||
|
<a href="#test2" alt="Sollte hier nicht stehen">Link mit falschem alt-Attribut</a>
|
||||||
|
|
||||||
|
<p>8. Bild mit potenziell gefährlichem alt-Text (XSS-Test):</p>
|
||||||
|
<img src="https://via.placeholder.com/100x80/000000/ffffff?text=8" alt="<img src=x onerror=alert('XSS')>">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue