fOERbico/docs/bildlizenzgenerator.html

1053 lines
39 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>