fOERbico/docs/bildlizenzgenerator.html

1053 lines
39 KiB
HTML
Raw Normal View History

2026-04-08 07:51:31 +02:00
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OER Bildlizenzgenerator TULLU+B</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300..700;1,9..40,300..700&family=JetBrains+Mono:wght@400;500&family=Fraunces:ital,opsz,wght@0,9..144,700;1,9..144,400&display=swap" rel="stylesheet">
<style>
/* ═══════════════════════════════════════════
Design Tokens
═══════════════════════════════════════════ */
:root {
--c-bg: #F7F5F0;
--c-surface: #FFFFFF;
--c-surface-alt: #F0EDE6;
--c-border: #DDD8CE;
--c-border-focus: #1D4ED8;
--c-text: #1A1A1A;
--c-text-muted: #6B6560;
--c-accent: #1D4ED8;
--c-accent-soft: #DBEAFE;
--c-accent-dark: #1E3A8A;
--c-success: #16A34A;
--c-success-bg: #DCFCE7;
--c-warn: #D97706;
--c-warn-bg: #FEF3C7;
--c-danger: #DC2626;
--c-danger-bg: #FEE2E2;
--c-highlight: #1E3A8A;
--font-body: 'DM Sans', system-ui, sans-serif;
--font-display: 'Fraunces', Georgia, serif;
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
--shadow-sm: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
--shadow-md: 0 4px 12px rgba(0,0,0,.07), 0 1px 3px rgba(0,0,0,.04);
--shadow-lg: 0 10px 30px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.04);
--transition: 200ms cubic-bezier(.4,0,.2,1);
}
/* ═══════════════════════════════════════════
Reset & Base
═══════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-body);
font-size: 15px;
line-height: 1.6;
color: var(--c-text);
background: var(--c-bg);
-webkit-font-smoothing: antialiased;
}
/* ═══════════════════════════════════════════
Layout
═══════════════════════════════════════════ */
.app-container {
max-width: 860px;
margin: 0 auto;
padding: 2rem 1.25rem 4rem;
}
/* ═══════════════════════════════════════════
Header
═══════════════════════════════════════════ */
.app-header {
text-align: center;
margin-bottom: 2.5rem;
position: relative;
}
.app-header::after {
content: '';
display: block;
width: 60px;
height: 3px;
background: var(--c-accent);
border-radius: 2px;
margin: 1.25rem auto 0;
}
.app-header h1 {
font-family: var(--font-display);
font-weight: 700;
font-size: clamp(1.6rem, 4vw, 2.2rem);
color: var(--c-text);
letter-spacing: -.02em;
line-height: 1.2;
}
.app-header h1 span {
background: linear-gradient(135deg, var(--c-accent), #6366F1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.app-header .subtitle {
margin-top: .5rem;
color: var(--c-text-muted);
font-size: .9rem;
}
.app-header .subtitle a {
color: var(--c-accent);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color var(--transition);
}
.app-header .subtitle a:hover { border-bottom-color: var(--c-accent); }
/* ═══════════════════════════════════════════
Card
═══════════════════════════════════════════ */
.card {
background: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
margin-bottom: 1.5rem;
transition: box-shadow var(--transition);
}
.card:hover { box-shadow: var(--shadow-md); }
.card-body { padding: 1.5rem; }
.card-title {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.15rem;
color: var(--c-text);
margin-bottom: 1.25rem;
display: flex;
align-items: center;
gap: .5rem;
}
.card-title .icon {
width: 28px; height: 28px;
background: var(--c-accent-soft);
border-radius: var(--radius-sm);
display: grid; place-items: center;
font-size: .85rem;
flex-shrink: 0;
}
/* ═══════════════════════════════════════════
Form Elements
═══════════════════════════════════════════ */
.form-label {
display: block;
font-size: .8rem;
font-weight: 600;
color: var(--c-text-muted);
text-transform: uppercase;
letter-spacing: .04em;
margin-bottom: .35rem;
}
.highlight-letter {
color: var(--c-highlight);
font-weight: 700;
font-size: .95em;
}
.form-control, .form-select {
width: 100%;
padding: .55rem .75rem;
font-family: var(--font-body);
font-size: .9rem;
color: var(--c-text);
background: var(--c-surface);
border: 1.5px solid var(--c-border);
border-radius: var(--radius-md);
outline: none;
transition: border-color var(--transition), box-shadow var(--transition);
}
.form-control:focus, .form-select:focus {
border-color: var(--c-border-focus);
box-shadow: 0 0 0 3px var(--c-accent-soft);
}
.form-control::placeholder { color: #B0AAA0; }
.form-control:disabled, .form-select:disabled {
background: var(--c-surface-alt);
opacity: .6;
cursor: not-allowed;
}
textarea.form-control { resize: vertical; min-height: 70px; }
.form-select {
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%236B6560' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right .6rem center;
background-size: 14px;
padding-right: 2rem;
}
.form-hint {
font-size: .78rem;
color: var(--c-text-muted);
margin-top: .3rem;
}
.form-check {
display: flex;
align-items: center;
gap: .5rem;
}
.form-check input[type="checkbox"] {
width: 18px; height: 18px;
accent-color: var(--c-accent);
cursor: pointer;
flex-shrink: 0;
}
.form-check label { font-size: .9rem; cursor: pointer; }
/* ═══════════════════════════════════════════
Grid Helpers
═══════════════════════════════════════════ */
.row { display: grid; gap: .75rem; }
.row-2 { grid-template-columns: 1fr 1fr; }
.row-3 { grid-template-columns: 1fr 1fr 1fr; }
.row-4 { grid-template-columns: 1fr 1fr 1fr 2fr; }
@media (max-width: 640px) {
.row-2, .row-3, .row-4 { grid-template-columns: 1fr; }
}
.field-group { display: flex; flex-direction: column; }
.field-group + .field-group { margin-top: .75rem; }
/* ═══════════════════════════════════════════
Author Rows
═══════════════════════════════════════════ */
.author-row {
display: grid;
grid-template-columns: 1.4fr 1fr auto;
gap: .5rem;
align-items: start;
animation: fadeSlideIn .25s ease;
}
@media (max-width: 640px) {
.author-row { grid-template-columns: 1fr auto; }
.author-row > :nth-child(2) { grid-column: 1; }
.author-row > :nth-child(3) { grid-row: 1; grid-column: 2; }
}
/* ═══════════════════════════════════════════
Buttons
═══════════════════════════════════════════ */
.btn {
display: inline-flex;
align-items: center;
gap: .4rem;
padding: .55rem 1.1rem;
font-family: var(--font-body);
font-size: .85rem;
font-weight: 600;
border: 1.5px solid transparent;
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition);
text-decoration: none;
line-height: 1.3;
}
.btn:active { transform: scale(.97); }
.btn-primary {
background: var(--c-accent);
color: #fff;
border-color: var(--c-accent);
}
.btn-primary:hover { background: var(--c-accent-dark); border-color: var(--c-accent-dark); }
.btn-secondary {
background: transparent;
color: var(--c-text-muted);
border-color: var(--c-border);
}
.btn-secondary:hover { background: var(--c-surface-alt); color: var(--c-text); }
.btn-icon {
width: 34px; height: 34px;
padding: 0; display: grid; place-items: center;
background: transparent;
color: var(--c-text-muted);
border-color: var(--c-border);
font-size: 1rem;
}
.btn-icon:hover { background: var(--c-danger-bg); color: var(--c-danger); border-color: var(--c-danger); }
.btn-sm { padding: .35rem .75rem; font-size: .8rem; }
.btn-group { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1.25rem; }
/* ═══════════════════════════════════════════
TULLU Badges
═══════════════════════════════════════════ */
.tullu-grid {
display: flex;
flex-wrap: wrap;
gap: .4rem;
}
.tullu-badge {
display: inline-flex;
align-items: center;
gap: .3rem;
padding: .3rem .65rem;
font-size: .78rem;
font-weight: 600;
border-radius: 999px;
transition: all var(--transition);
}
.tullu-badge.ok { background: var(--c-success-bg); color: #166534; }
.tullu-badge.missing { background: var(--c-warn-bg); color: #92400E; }
.tullu-badge.neutral { background: var(--c-surface-alt); color: var(--c-text-muted); }
/* ═══════════════════════════════════════════
Output Tabs
═══════════════════════════════════════════ */
.output-tabs {
display: flex;
border-bottom: 2px solid var(--c-border);
margin-bottom: 1rem;
gap: 0;
overflow-x: auto;
}
.output-tab {
padding: .55rem 1rem;
font-size: .82rem;
font-weight: 600;
color: var(--c-text-muted);
background: transparent;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
cursor: pointer;
white-space: nowrap;
transition: all var(--transition);
font-family: var(--font-body);
}
.output-tab:hover { color: var(--c-text); }
.output-tab.active {
color: var(--c-accent);
border-bottom-color: var(--c-accent);
}
.output-panel { display: none; animation: fadeIn .2s ease; }
.output-panel.active { display: block; }
.output-area { position: relative; }
.output-area textarea {
min-height: 110px;
font-family: var(--font-mono);
font-size: .82rem;
line-height: 1.55;
background: var(--c-surface-alt);
border-color: var(--c-border);
}
.output-area textarea:focus {
background: var(--c-surface);
}
/* Copy Button */
.copy-btn {
position: absolute;
top: .5rem; right: .5rem;
padding: .3rem .6rem;
font-size: .75rem;
font-weight: 600;
font-family: var(--font-body);
background: var(--c-surface);
border: 1.5px solid var(--c-border);
border-radius: var(--radius-sm);
color: var(--c-text-muted);
cursor: pointer;
transition: all var(--transition);
z-index: 2;
}
.copy-btn:hover { background: var(--c-accent-soft); color: var(--c-accent); border-color: var(--c-accent); }
.copy-btn.copied { background: var(--c-success-bg); color: var(--c-success); border-color: var(--c-success); }
/* ═══════════════════════════════════════════
Preview Figure
═══════════════════════════════════════════ */
.preview-figure {
border: 1px solid var(--c-border);
border-radius: var(--radius-md);
padding: 1rem;
background: var(--c-surface-alt);
margin-top: .75rem;
}
.preview-figure img {
max-width: 100%;
border-radius: var(--radius-sm);
margin-bottom: .75rem;
display: none;
}
.preview-figure figcaption {
font-size: .85rem;
line-height: 1.6;
color: var(--c-text);
}
.preview-figure figcaption a {
color: var(--c-accent);
text-decoration: none;
}
.preview-figure figcaption a:hover {
text-decoration: underline;
}
.preview-label {
font-size: .75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .04em;
color: var(--c-text-muted);
margin-bottom: .5rem;
}
.md-preview {
font-size: .85rem;
line-height: 1.65;
color: var(--c-text);
}
.md-preview strong { font-weight: 700; }
.md-preview a {
color: var(--c-accent);
text-decoration: none;
}
.md-preview a:hover { text-decoration: underline; }
/* ═══════════════════════════════════════════
Footer
═══════════════════════════════════════════ */
.app-footer {
text-align: center;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--c-border);
font-size: .78rem;
color: var(--c-text-muted);
}
/* ═══════════════════════════════════════════
Animations
═══════════════════════════════════════════ */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes fadeSlideIn { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
</style>
</head>
<body>
<div class="app-container">
<!-- ─── Header ─── -->
<header class="app-header">
<h1>OER Bildlizenzgenerator <span>TULLU+B</span></h1>
<p class="subtitle">
OER-Bildnachweise nach der
<a href="https://www.orca.nrw/oer/oer-nutzen/tulluba-regel/" target="_blank" rel="noopener">TULLUBA-Regel</a>
— mit Klartext, HTML, Markdown &amp; JSON-LD
</p>
</header>
<!-- ─── Eingaben ─── -->
<section class="card" id="inputCard">
<div class="card-body">
<h2 class="card-title"><span class="icon">✏️</span> Eingaben</h2>
<div class="field-group">
<label class="form-label" for="title"><span class="highlight-letter">T</span>itel des Werks</label>
<input id="title" class="form-control" placeholder="z. B. Sonnenuntergang über Berlin">
</div>
<div class="field-group">
<label class="form-label"><span class="highlight-letter">U</span>rheber:in / Autor:in</label>
<div id="authors"></div>
<button id="addAuthorBtn" type="button" class="btn btn-secondary btn-sm" style="margin-top:.5rem">+ Autor:in</button>
<p class="form-hint">Mehrere Autor:innen möglich. Leere Zeilen werden ignoriert.</p>
</div>
<div class="row row-2">
<div class="field-group">
<label class="form-label" for="license"><span class="highlight-letter">L</span>izenz</label>
<select id="license" class="form-select"></select>
</div>
<div class="field-group">
<label class="form-label" for="licenseVersion">Version</label>
<select id="licenseVersion" class="form-select">
<option value="4.0" selected>4.0</option>
<option value="3.0">3.0</option>
<option value="2.0">2.0</option>
<option value="1.0">1.0</option>
<option value="">(keine / CC0 / PDM)</option>
</select>
</div>
</div>
<div class="row row-2" style="margin-top:.75rem">
<div class="field-group">
<label class="form-label" for="workUrl"><span class="highlight-letter">L</span>ink zum Werk</label>
<input id="workUrl" class="form-control" placeholder="https://…">
</div>
<div class="field-group">
<label class="form-label" for="source"><span class="highlight-letter">U</span>rsprungsort / Plattform</label>
<input id="source" class="form-control" placeholder="Wikimedia Commons, Flickr …">
</div>
</div>
<div class="row row-4" style="margin-top:.75rem">
<div class="field-group">
<label class="form-label" for="year">Jahr</label>
<input id="year" class="form-control" placeholder="2025" inputmode="numeric">
</div>
<div class="field-group">
<label class="form-label" for="lang">Sprache</label>
<select id="lang" class="form-select">
<option value="de" selected>Deutsch</option>
<option value="en">English</option>
</select>
</div>
<div class="field-group" style="grid-column: span 2">
<label class="form-label" for="extra">Zusätzliche Hinweise</label>
<input id="extra" class="form-control" placeholder="z. B. Bildzuschnitt durch die Redaktion">
</div>
</div>
<div class="field-group" style="margin-top:.75rem">
<div class="form-check">
<input type="checkbox" id="isAdapted">
<label for="isAdapted"><span class="highlight-letter">B</span>earbeitung / Anpassung vorhanden</label>
</div>
<textarea id="adaptationNote" class="form-control" style="margin-top:.5rem"
placeholder="z. B. Zuschnitt, Farbkorrektur; Text hinzugefügt" disabled></textarea>
</div>
<div class="btn-group">
<button id="buildBtn" type="button" class="btn btn-primary">▶ Lizenzhinweis erzeugen</button>
<button id="exampleBtn" type="button" class="btn btn-secondary">Beispiel laden</button>
<button id="resetBtn" type="button" class="btn btn-secondary">Zurücksetzen</button>
</div>
<p class="form-hint" style="margin-top:.75rem">
Pflicht bei CC BY / CC BY-SA: Urheber:in, Lizenzname und -link. Bei Bearbeitungen immer kenntlich machen (+B).
</p>
</div>
</section>
<!-- ─── TULLU-Check ─── -->
<section class="card" id="checkCard">
<div class="card-body">
<h2 class="card-title"><span class="icon"></span> TULLU-Check</h2>
<div class="tullu-grid" id="tulluBadges" aria-live="polite"></div>
</div>
</section>
<!-- ─── Ausgabe ─── -->
<section class="card" id="outputCard">
<div class="card-body">
<h2 class="card-title"><span class="icon">📋</span> Ausgabe</h2>
<div class="output-tabs" role="tablist">
<button class="output-tab active" data-tab="plaintext" role="tab">Klartext</button>
<button class="output-tab" data-tab="figcaption" role="tab">HTML-Figcaption</button>
<button class="output-tab" data-tab="markdown" role="tab">Markdown</button>
<button class="output-tab" data-tab="jsonld" role="tab">JSON-LD</button>
</div>
<!-- Klartext -->
<div class="output-panel active" data-panel="plaintext">
<p style="font-size:.82rem;color:var(--c-text-muted);margin-bottom:.5rem">Für Impressum, Bildnachweis-Listen oder Alt-Text.</p>
<div class="output-area">
<button class="copy-btn" data-target="outText">Kopieren</button>
<textarea id="outText" class="form-control" readonly></textarea>
</div>
</div>
<!-- Figcaption -->
<div class="output-panel" data-panel="figcaption">
<p style="font-size:.82rem;color:var(--c-text-muted);margin-bottom:.5rem">HTML-Snippet zum Einfügen unter Bildern.</p>
<div class="output-area">
<button class="copy-btn" data-target="outFigcaption">Kopieren</button>
<textarea id="outFigcaption" class="form-control" readonly></textarea>
</div>
<figure class="preview-figure">
<p class="preview-label">Vorschau (gerendert):</p>
<img id="previewImage" src="#" alt="Vorschau-Bild">
<figcaption id="figcaptionPreview"></figcaption>
</figure>
</div>
<!-- Markdown -->
<div class="output-panel" data-panel="markdown">
<p style="font-size:.82rem;color:var(--c-text-muted);margin-bottom:.5rem">Für README-Dateien, Wiki-Seiten, Markdown-Dokumente.</p>
<div class="output-area">
<button class="copy-btn" data-target="outMarkdown">Kopieren</button>
<textarea id="outMarkdown" class="form-control" readonly></textarea>
</div>
<div class="preview-figure" id="markdownPreview">
<p class="preview-label">Vorschau (gerendert):</p>
<div id="markdownPreviewContent" class="md-preview"></div>
</div>
</div>
<!-- JSON-LD -->
<div class="output-panel" data-panel="jsonld">
<p style="font-size:.82rem;color:var(--c-text-muted);margin-bottom:.5rem">Strukturierte Daten für SEO &amp; maschinelle Auswertung.</p>
<div class="output-area">
<button class="copy-btn" data-target="outJsonLd">Kopieren</button>
<textarea id="outJsonLd" class="form-control" readonly style="min-height:180px"></textarea>
</div>
</div>
</div>
</section>
<footer class="app-footer">
Generiert nach der <a href="https://www.orca.nrw/oer/oer-nutzen/tulluba-regel/" target="_blank" rel="noopener">TULLUBA-Regel</a> ·
Keine Gewähr für rechtliche Vollständigkeit
</footer>
</div>
<script>
/* ═══════════════════════════════════════════════════════
TULLU+B Bildlizenzgenerator Refactored
═══════════════════════════════════════════════════════ */
// ─── License Database ───────────────────────────────
const LICENSES = [
{ id: 'CC0', label: 'CC0 (Public Domain Waiver)', hasVersion: false, urlFn: () => 'https://creativecommons.org/publicdomain/zero/1.0/' },
{ id: 'PDM', label: 'Public Domain Mark (PDM)', hasVersion: false, urlFn: () => 'https://creativecommons.org/publicdomain/mark/1.0/' },
{ id: 'BY', label: 'CC BY', hasVersion: true, urlFn: (v) => `https://creativecommons.org/licenses/by/${v || '4.0'}/` },
{ id: 'BY-SA', label: 'CC BY-SA', hasVersion: true, urlFn: (v) => `https://creativecommons.org/licenses/by-sa/${v || '4.0'}/` },
{ id: 'BY-NC', label: 'CC BY-NC', hasVersion: true, urlFn: (v) => `https://creativecommons.org/licenses/by-nc/${v || '4.0'}/` },
{ id: 'BY-NC-SA', label: 'CC BY-NC-SA', hasVersion: true, urlFn: (v) => `https://creativecommons.org/licenses/by-nc-sa/${v || '4.0'}/` },
{ id: 'BY-ND', label: 'CC BY-ND', hasVersion: true, urlFn: (v) => `https://creativecommons.org/licenses/by-nd/${v || '4.0'}/` },
{ id: 'BY-NC-ND', label: 'CC BY-NC-ND', hasVersion: true, urlFn: (v) => `https://creativecommons.org/licenses/by-nc-nd/${v || '4.0'}/` },
{ id: 'CUSTOM', label: 'Benutzerdefiniert', hasVersion: true, urlFn: () => null },
];
const LICENSE_ALIASES = Object.fromEntries([
['cc0','CC0'], ['pdm','PDM'],
['by','BY'], ['cc-by','BY'],
['by-sa','BY-SA'], ['cc-by-sa','BY-SA'], ['by_sa','BY-SA'],
['by-nc','BY-NC'], ['cc-by-nc','BY-NC'], ['by_nc','BY-NC'],
['by-nc-sa','BY-NC-SA'], ['cc-by-nc-sa','BY-NC-SA'], ['by_nc_sa','BY-NC-SA'],
['by-nd','BY-ND'], ['cc-by-nd','BY-ND'], ['by_nd','BY-ND'],
['by-nc-nd','BY-NC-ND'], ['cc-by-nc-nd','BY-NC-ND'], ['by_nc_nd','BY-NC-ND'],
['custom','CUSTOM'],
].map(([k, v]) => [k, v]));
// ─── Utilities ──────────────────────────────────────
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(sel);
function escapeHTML(str) {
const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
return String(str).replace(/[&<>"']/g, (ch) => map[ch]);
}
function escapeAttr(str) {
return String(str).replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function getLicenseDef(id) {
return LICENSES.find((l) => l.id === id) || LICENSES[0];
}
function getLicenseLabel(licDef, version) {
if (licDef.id === 'CC0') return 'CC0 1.0';
if (licDef.id === 'PDM') return 'Public Domain Mark 1.0';
if (licDef.id === 'CUSTOM') return 'Benutzerdefinierte Lizenz';
return `${licDef.label} ${version || ''}`.trim();
}
// ─── Author Management ──────────────────────────────
const authorsContainer = $('#authors');
function addAuthor(name = '', url = '') {
const row = document.createElement('div');
row.className = 'author-row';
row.innerHTML = `
<input class="form-control" placeholder="Name" value="${escapeAttr(name)}">
<input class="form-control" placeholder="URL (optional)" value="${escapeAttr(url)}">
<button type="button" class="btn btn-icon" title="Entfernen"></button>
`;
row.querySelector('button').addEventListener('click', () => {
row.remove();
ensureOneAuthor();
});
authorsContainer.appendChild(row);
}
function getAuthors() {
return Array.from(authorsContainer.querySelectorAll('.author-row'))
.map((row) => {
const inputs = row.querySelectorAll('input');
const name = inputs[0].value.trim();
const url = inputs[1].value.trim();
return name ? { name, url: url || null } : null;
})
.filter(Boolean);
}
function clearAuthors() { authorsContainer.innerHTML = ''; }
function ensureOneAuthor() { if (!authorsContainer.querySelector('.author-row')) addAuthor(); }
// ─── Populate License Select ────────────────────────
const licenseSelect = $('#license');
LICENSES.forEach((lic) => {
const opt = document.createElement('option');
opt.value = lic.id;
opt.textContent = lic.label;
licenseSelect.appendChild(opt);
});
licenseSelect.value = 'BY';
// ─── Read Form Data ─────────────────────────────────
function readForm() {
return {
title: $('#title').value.trim(),
creators: getAuthors(),
licenseId: $('#license').value,
licenseVersion: $('#licenseVersion').value,
workUrl: $('#workUrl').value.trim(),
source: $('#source').value.trim(),
year: $('#year').value.trim(),
isAdapted: $('#isAdapted').checked,
adaptationNote: $('#adaptationNote').value.trim(),
extra: $('#extra').value.trim(),
lang: $('#lang').value,
};
}
// ─── Attribution Formatting ─────────────────────────
function formatAttribution(data) {
const licDef = getLicenseDef(data.licenseId);
const licUrl = licDef.urlFn(data.licenseVersion) || '';
const licLabel = getLicenseLabel(licDef, data.licenseVersion);
const isDE = data.lang === 'de';
const title = data.title || (isDE ? '(ohne Titel)' : '(untitled)');
const unknownCreator = isDE ? '(Urheber:in unbekannt)' : '(creator unknown)';
// ── Creator Strings ──
const creatorsPlain = data.creators.length
? data.creators.map((c) => c.url ? `${c.name} (${c.url})` : c.name).join(', ')
: unknownCreator;
const creatorsHTML = data.creators.length
? data.creators.map((c) => c.url
? `<a href="${escapeAttr(c.url)}" target="_blank" rel="noopener">${escapeHTML(c.name)}</a>`
: escapeHTML(c.name)
).join(', ')
: unknownCreator;
const creatorsMD = data.creators.length
? data.creators.map((c) => c.url ? `[${c.name}](${c.url})` : c.name).join(', ')
: unknownCreator;
// Labels
const lbl = {
by: isDE ? 'von' : 'by',
lic: isDE ? 'lizenziert unter' : 'licensed under',
licC: isDE ? 'Lizenz' : 'License',
src: isDE ? 'Quelle' : 'Source',
link: 'Link',
changes: isDE ? 'Änderungen' : 'Changes',
title: 'Titel',
author: isDE ? 'Urheber:in' : 'Creator',
origin: isDE ? 'Ursprungsort' : 'Source',
};
const adaptNote = data.adaptationNote || (isDE ? 'Bearbeitung vorhanden' : 'Adaptation present');
// ── 1) Klartext ──
const textParts = [
`„${title}"`,
`${lbl.by} ${creatorsPlain}`,
];
if (data.year) textParts.push(data.year);
textParts.push(
licDef.id === 'CUSTOM'
? `${lbl.licC}: ${licLabel}`
: `${lbl.lic} ${licLabel}`
);
if (data.source) textParts.push(`${lbl.src}: ${data.source}`);
if (data.workUrl) textParts.push(`${lbl.link}: ${data.workUrl}`);
if (data.isAdapted) textParts.push(`${lbl.changes}: ${adaptNote}`);
if (data.extra) textParts.push(data.extra);
const plainText = textParts.join(' · ');
// ── 2) HTML Figcaption ──
const licHtml = licDef.id === 'CUSTOM'
? escapeHTML(licLabel)
: `<a href="${licUrl}" target="_blank" rel="license noopener">${escapeHTML(licLabel)}</a>`;
const figLines = [
`${lbl.title}: „${escapeHTML(data.title || '')}"`,
`${lbl.author}: ${creatorsHTML}`,
`${lbl.licC}: ${licHtml}`,
`${lbl.origin}: ${data.source ? escapeHTML(data.source) : ''}`,
`${lbl.link}: ${data.workUrl ? `<a href="${data.workUrl}" target="_blank" rel="noopener">${escapeHTML(data.workUrl)}</a>` : ''}`,
];
if (data.isAdapted) figLines.push(`${lbl.changes}: ${escapeHTML(adaptNote)}`);
if (data.extra) figLines.push(escapeHTML(data.extra));
const figcaptionInner = figLines.join('<br>\n ');
const figcaptionFull = `<figcaption>\n ${figcaptionInner}\n</figcaption>`;
// ── 3) Markdown ──
const licMD = licDef.id === 'CUSTOM'
? licLabel
: licUrl ? `[${licLabel}](${licUrl})` : licLabel;
const mdLines = [
`**${lbl.title}:** „${data.title || ''}" `,
`**${lbl.author}:** ${creatorsMD} `,
`**${lbl.licC}:** ${licMD} `,
`**${lbl.origin}:** ${data.source || ''} `,
`**${lbl.link}:** ${data.workUrl ? `<${data.workUrl}>` : ''} `,
];
if (data.isAdapted) mdLines.push(`**${lbl.changes}:** ${adaptNote} `);
if (data.extra) mdLines.push(data.extra + ' ');
const markdown = mdLines.join('\n');
// ── 4) JSON-LD ──
const creatorsJson = data.creators.map((c) => ({
'@type': 'Person',
name: c.name,
...(c.url ? { url: c.url } : {}),
}));
const jsonObj = {
'@context': 'https://schema.org',
'@type': 'ImageObject',
name: data.title || '',
...(data.workUrl ? { contentUrl: data.workUrl } : {}),
...(data.source ? { isBasedOn: data.source } : {}),
...(data.year ? { dateCreated: data.year } : {}),
creator: creatorsJson,
...(licUrl ? { license: licUrl } : {}),
};
if (data.isAdapted) jsonObj.creditText = `Adaptation: ${adaptNote}`;
if (data.extra) jsonObj.comment = data.extra;
const jsonLD = JSON.stringify(jsonObj, null, 2);
// ── TULLU Check ──
const check = {
T: !!data.title,
U: data.creators.length > 0,
L: !!licLabel,
L2: licDef.id === 'CUSTOM' || !!licUrl,
U2: !!data.workUrl,
U3: !!data.source,
B: data.isAdapted ? !!data.adaptationNote : null,
};
return { plainText, figcaptionFull, figcaptionInner, markdown, jsonLD, check };
}
// ─── TULLU Badges ───────────────────────────────────
const TULLU_ITEMS = [
['T', 'Titel'],
['U', 'Urheber:in'],
['L', 'Lizenz'],
['L2', 'Lizenz-Link'],
['U2', 'Werk-Link'],
['U3', 'Ursprungsort'],
['B', '+ Bearbeitung'],
];
function renderBadges(check) {
const container = $('#tulluBadges');
container.innerHTML = '';
TULLU_ITEMS.forEach(([key, label]) => {
const val = check[key];
const badge = document.createElement('span');
if (val === null || val === undefined) {
badge.className = 'tullu-badge neutral';
badge.textContent = `${label} `;
} else if (val) {
badge.className = 'tullu-badge ok';
badge.textContent = `${label} ✓`;
} else {
badge.className = 'tullu-badge missing';
badge.textContent = `${label} ○`;
}
container.appendChild(badge);
});
}
// ─── Simple Markdown → HTML (for preview) ───────────
function renderMarkdownToHTML(md) {
return md
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
.replace(/<(https?:\/\/[^>]+)>/g, '<a href="$1" target="_blank" rel="noopener">$1</a>')
.replace(/„([^"]+)"/g, '„$1"')
.replace(/ \n/g, '<br>\n')
.replace(/ $/gm, '<br>');
}
// ─── Build All Outputs ──────────────────────────────
function buildAll() {
const data = readForm();
const result = formatAttribution(data);
$('#outText').value = result.plainText;
$('#outFigcaption').value = result.figcaptionFull;
$('#outMarkdown').value = result.markdown;
$('#outJsonLd').value = result.jsonLD;
$('#figcaptionPreview').innerHTML = result.figcaptionInner;
$('#markdownPreviewContent').innerHTML = renderMarkdownToHTML(result.markdown);
const img = $('#previewImage');
if (data.workUrl) {
img.src = data.workUrl;
img.style.display = 'block';
} else {
img.style.display = 'none';
}
renderBadges(result.check);
}
// ─── Tab Switching ──────────────────────────────────
$$('.output-tab').forEach((tab) => {
tab.addEventListener('click', () => {
$$('.output-tab').forEach((t) => t.classList.remove('active'));
$$('.output-panel').forEach((p) => p.classList.remove('active'));
tab.classList.add('active');
const panel = $(`[data-panel="${tab.dataset.tab}"]`);
if (panel) panel.classList.add('active');
});
});
// ─── Copy Buttons ───────────────────────────────────
$$('.copy-btn').forEach((btn) => {
btn.addEventListener('click', () => {
const textarea = $(`#${btn.dataset.target}`);
if (!textarea) return;
navigator.clipboard.writeText(textarea.value).then(() => {
btn.textContent = '✓ Kopiert!';
btn.classList.add('copied');
setTimeout(() => {
btn.textContent = 'Kopieren';
btn.classList.remove('copied');
}, 1500);
});
});
});
// ─── Event Listeners ────────────────────────────────
$('#isAdapted').addEventListener('change', (e) => {
const note = $('#adaptationNote');
note.disabled = !e.target.checked;
if (!e.target.checked) note.value = '';
});
$('#license').addEventListener('change', () => {
const def = getLicenseDef(licenseSelect.value);
const versionSel = $('#licenseVersion');
versionSel.disabled = !def.hasVersion;
if (!def.hasVersion) versionSel.value = '';
});
$('#buildBtn').addEventListener('click', buildAll);
$('#addAuthorBtn').addEventListener('click', () => addAuthor());
$('#exampleBtn').addEventListener('click', () => {
$('#title').value = 'Byzantion - Dionysos und Vogel Strauß';
clearAuthors();
addAuthor('Reinhard Saczewski', '');
$('#license').value = 'PDM';
$('#license').dispatchEvent(new Event('change'));
$('#licenseVersion').value = '';
$('#workUrl').value = 'https://commons.wikimedia.org/wiki/File:Byzantion_-_M%C3%BCnzkabinett,_Berlin_-_5533682.jpg';
$('#source').value = 'Wikimedia Commons';
$('#year').value = '2022';
$('#isAdapted').checked = false;
$('#adaptationNote').disabled = true;
$('#adaptationNote').value = '';
$('#extra').value = '';
$('#lang').value = 'de';
buildAll();
});
$('#resetBtn').addEventListener('click', () => {
['title', 'workUrl', 'source', 'year', 'extra'].forEach((id) => $(`#${id}`).value = '');
$('#isAdapted').checked = false;
$('#adaptationNote').value = '';
$('#adaptationNote').disabled = true;
clearAuthors();
ensureOneAuthor();
$('#license').value = 'BY';
$('#licenseVersion').value = '4.0';
$('#licenseVersion').disabled = false;
$('#lang').value = 'de';
['outText', 'outFigcaption', 'outMarkdown', 'outJsonLd'].forEach((id) => $(`#${id}`).value = '');
$('#figcaptionPreview').innerHTML = '';
$('#markdownPreviewContent').innerHTML = '';
$('#previewImage').style.display = 'none';
renderBadges({});
});
// ─── URL Prefill ────────────────────────────────────
function applyQueryParams() {
const params = new URLSearchParams(location.search);
if (![...params.keys()].length) return;
const setVal = (id, val) => {
if (val) $(`#${id}`).value = val;
};
setVal('title', params.get('title') || params.get('t'));
// Authors
let authorEntries = params.getAll('author');
if (!authorEntries.length) {
const agg = params.get('authors');
if (agg) authorEntries = agg.split(';').map((s) => s.trim()).filter(Boolean);
}
if (authorEntries.length) {
clearAuthors();
authorEntries.forEach((entry) => {
const [name, url] = entry.split('|');
addAuthor((name || '').trim(), (url || '').trim());
});
}
// License
let lic = params.get('license') || params.get('lic') || params.get('l');
if (lic) {
lic = lic.toLowerCase().replace(/\s+/g, '').replace(/_/g, '-');
const normalized = LICENSE_ALIASES[lic] || lic.toUpperCase();
if (LICENSES.some((l) => l.id === normalized)) licenseSelect.value = normalized;
}
const ver = params.get('licenseVersion') || params.get('lv') || params.get('v');
if (ver != null) setVal('licenseVersion', ver);
setVal('workUrl', params.get('work') || params.get('workUrl') || params.get('url') || params.get('link'));
setVal('source', params.get('source') || params.get('sourceUrl') || params.get('src'));
setVal('year', params.get('year') || params.get('y'));
const lang = (params.get('lang') || '').toLowerCase();
if (lang === 'en' || lang === 'de') setVal('lang', lang);
setVal('extra', params.get('extra') || params.get('note'));
const adapted = params.get('adapted') || params.get('b') || params.get('isAdapted');
if (adapted && /^(1|true|yes|ja|y)$/i.test(adapted)) {
$('#isAdapted').checked = true;
$('#adaptationNote').disabled = false;
}
setVal('adaptationNote', params.get('changes') || params.get('adaptation') || params.get('adaptationNote'));
}
// ─── Init ───────────────────────────────────────────
function init() {
ensureOneAuthor();
licenseSelect.dispatchEvent(new Event('change'));
applyQueryParams();
buildAll();
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>