|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | + <meta charset="UTF-8"> |
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 6 | + <title>Clipboard RTF to HTML Inspector</title> |
| 7 | + <style> |
| 8 | + body { font-family: Arial, sans-serif; margin: 20px; } |
| 9 | + h1 { font-size: 1.5em; } |
| 10 | + #controls { margin-bottom: 10px; } |
| 11 | + #output { margin-top: 20px; } |
| 12 | + .format-block { border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; } |
| 13 | + .format-label { font-weight: bold; margin-bottom: 5px; } |
| 14 | + .preview { white-space: pre-wrap; word-wrap: break-word; background: #f9f9f9; padding: 10px; font-family: 'Courier New', monospace; } |
| 15 | + textarea { width: 100%; box-sizing: border-box; font-family: 'Courier New', monospace; resize: vertical; } |
| 16 | + /* Dark mode styles */ |
| 17 | + .dark .preview { background: #000; color: #fff; } |
| 18 | + .dark textarea { background: #000; color: #fff; } |
| 19 | + .dark .format-block { border-color: #555; } |
| 20 | + </style> |
| 21 | +</head> |
| 22 | +<body> |
| 23 | + <h1>Clipboard RTF to HTML Inspector</h1> |
| 24 | + <div id="controls"> |
| 25 | + <label><input type="checkbox" id="dark-mode"> Black background</label> |
| 26 | + <p>Press <strong>Cmd+V</strong> (Mac) or <strong>Ctrl+V</strong> (Windows) to paste and convert RTF to colorized HTML.</p> |
| 27 | + </div> |
| 28 | + <div id="output"></div> |
| 29 | + |
| 30 | + <script> |
| 31 | + const darkModeCheckbox = document.getElementById('dark-mode'); |
| 32 | + darkModeCheckbox.addEventListener('change', () => { |
| 33 | + document.body.classList.toggle('dark', darkModeCheckbox.checked); |
| 34 | + }); |
| 35 | + |
| 36 | + document.addEventListener('paste', function(event) { |
| 37 | + event.preventDefault(); |
| 38 | + const output = document.getElementById('output'); |
| 39 | + output.innerHTML = ''; |
| 40 | + |
| 41 | + const clipboardData = event.clipboardData || window.clipboardData; |
| 42 | + const rtf = clipboardData.getData('text/rtf'); |
| 43 | + if (!rtf) { |
| 44 | + output.textContent = 'No RTF data available in clipboard.'; |
| 45 | + return; |
| 46 | + } |
| 47 | + |
| 48 | + // Normalize line breaks |
| 49 | + let body = rtf.replace(/\r\n|\r/g, '\n'); |
| 50 | + |
| 51 | + // Parse color table (colors[1] is first color) |
| 52 | + const colorTableMatch = /\\colortbl;([^}]*)}/.exec(body); |
| 53 | + const colors = [null]; |
| 54 | + if (colorTableMatch) { |
| 55 | + const defs = colorTableMatch[1].split(';'); |
| 56 | + defs.forEach(def => { |
| 57 | + const m = /\\red(\d+)\\green(\d+)\\blue(\d+)/.exec(def); |
| 58 | + if (m) { |
| 59 | + const r = +m[1], g = +m[2], b = +m[3]; |
| 60 | + const hex = '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); |
| 61 | + colors.push(hex); |
| 62 | + } else { |
| 63 | + colors.push(null); |
| 64 | + } |
| 65 | + }); |
| 66 | + } |
| 67 | + |
| 68 | + // Mark paragraphs & remove controls |
| 69 | + body = body.replace(/\\pard[d]?/g, '\n') |
| 70 | + .replace(/\\cf(\d+)/g, '[CF$1]') |
| 71 | + .replace(/\\[^ \n]+ ?/g, '') |
| 72 | + .replace(/[{}]/g, '') |
| 73 | + .replace(/\\/g, ''); |
| 74 | + |
| 75 | + // Wrap color runs |
| 76 | + const parts = body.split(/\[CF(\d+)\]/); |
| 77 | + let currentColor = null; |
| 78 | + let html = ''; |
| 79 | + parts.forEach((seg, i) => { |
| 80 | + if (i % 2 === 1) { |
| 81 | + currentColor = +seg; |
| 82 | + } else { |
| 83 | + const text = seg.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); |
| 84 | + if (currentColor && colors[currentColor]) { |
| 85 | + html += `<span style="color:${colors[currentColor]}">${text}</span>`; |
| 86 | + } else { |
| 87 | + html += text; |
| 88 | + } |
| 89 | + } |
| 90 | + }); |
| 91 | + |
| 92 | + // Newlines to <br> |
| 93 | + const htmlWithBreaks = html.split('\n').join('<br>'); |
| 94 | + |
| 95 | + // Preview |
| 96 | + const previewBlock = document.createElement('div'); |
| 97 | + previewBlock.className = 'format-block'; |
| 98 | + const previewLabel = document.createElement('div'); |
| 99 | + previewLabel.className = 'format-label'; |
| 100 | + previewLabel.textContent = 'Preview'; |
| 101 | + const previewDiv = document.createElement('div'); |
| 102 | + previewDiv.className = 'preview'; |
| 103 | + previewDiv.innerHTML = htmlWithBreaks; |
| 104 | + previewBlock.append(previewLabel, previewDiv); |
| 105 | + output.appendChild(previewBlock); |
| 106 | + |
| 107 | + // HTML markup |
| 108 | + const htmlBlock = document.createElement('div'); |
| 109 | + htmlBlock.className = 'format-block'; |
| 110 | + const htmlLabel = document.createElement('div'); |
| 111 | + htmlLabel.className = 'format-label'; |
| 112 | + htmlLabel.textContent = 'HTML Markup'; |
| 113 | + const textarea = document.createElement('textarea'); |
| 114 | + textarea.rows = 10; |
| 115 | + textarea.value = htmlWithBreaks; |
| 116 | + htmlBlock.append(htmlLabel, textarea); |
| 117 | + output.appendChild(htmlBlock); |
| 118 | + }); |
| 119 | + </script> |
| 120 | +</body> |
| 121 | +</html> |
0 commit comments