diff --git a/at/user-page.html b/at/user-page.html index cec700e11e..ae8f5cb5c8 100644 --- a/at/user-page.html +++ b/at/user-page.html @@ -375,6 +375,65 @@ Uses ONLY ATProto APIs - no aesthetic.computer backend dependencies to { transform: scale(1); opacity: 1; } } + /* Preview box scrolling animation */ + .source-preview { + position: relative; + max-height: 120px; + overflow: hidden; + background: linear-gradient(135deg, rgba(205, 92, 155, 0.05), rgba(100, 180, 255, 0.05)); + border: 1px solid rgba(205, 92, 155, 0.2); + } + + .source-preview:hover .preview-scroll-container { + animation-play-state: paused !important; + } + + .preview-scroll-container { + display: flex; + animation: previewScrollLoop 40s linear infinite; + will-change: transform; + } + + .line-numbers { + flex-shrink: 0; + width: 3em; + text-align: right; + padding-right: 0.5em; + user-select: none; + font-size: 0.9em; + line-height: inherit; + background: rgba(0, 0, 0, 0.1); + border-right: 1px solid rgba(0, 0, 0, 0.15); + } + + .line-numbers div { + padding: 0.1em 0; + } + + /* Striped color palette for line numbers */ + .line-numbers div:nth-child(6n+1) { color: rgb(205, 92, 155); opacity: 0.8; } + .line-numbers div:nth-child(6n+2) { color: rgb(255, 120, 100); opacity: 0.8; } + .line-numbers div:nth-child(6n+3) { color: rgb(255, 200, 100); opacity: 0.8; } + .line-numbers div:nth-child(6n+4) { color: rgb(150, 230, 150); opacity: 0.8; } + .line-numbers div:nth-child(6n+5) { color: rgb(100, 180, 255); opacity: 0.8; } + .line-numbers div:nth-child(6n+6) { color: rgb(180, 130, 230); opacity: 0.8; } + + .preview-content { + display: block; + flex: 1; + padding-left: 0.5em; + } + + /* Infinite scroll - content is duplicated so it wraps seamlessly */ + @keyframes previewScrollLoop { + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(-50%); + } + } + .empty-state { text-align: center; padding: 2em 1em; @@ -684,29 +743,45 @@ Uses ONLY ATProto APIs - no aesthetic.computer backend dependencies 'red': '#ff0000', 'darkred': '#8b0000', 'orange': '#ff6600', - 'yellow': isDark ? '#ffff00' : '#d4a000', // Darker yellow for light mode - 'lime': isDark ? '#00ff00' : '#00a000', // Darker lime for light mode + 'yellow': isDark ? '#ffff00' : '#d4a000', + 'lime': isDark ? '#00ff00' : '#00a000', 'limegreen': '#32cd32', 'green': '#008000', - 'cyan': isDark ? '#00ffff' : '#008b8b', // Darker cyan for light mode + 'cyan': isDark ? '#00ffff' : '#008b8b', 'blue': '#0000ff', 'purple': '#800080', 'magenta': '#ff00ff', 'hotpink': '#ff69b4', - 'white': isDark ? '#ffffff' : '#333333', // Dark gray for light mode - 'black': isDark ? '#000000' : '#ffffff', // Invert for light mode + 'white': isDark ? '#ffffff' : '#333333', + 'black': isDark ? '#000000' : '#ffffff', 'gray': '#808080', 'grey': '#808080', 'mediumseagreen': '#3cb371', 'pink': '#ffc0cb', 'brown': '#a52a2a', 'violet': '#ee82ee', - '192,192,192': isDark ? 'rgb(192,192,192)' : 'rgb(100,100,100)' // Gray values + 'teal': '#008080', + 'navy': '#000080', + 'maroon': '#800000', + 'olive': '#808000', + 'silver': '#c0c0c0' }; const result = []; let i = 0; - let currentColor = isDark ? '#ffffff' : '#333333'; // Default color based on theme + let currentColor = isDark ? '#ffffff' : '#333333'; + let textBuffer = ''; + + function flushBuffer() { + if (textBuffer) { + const escaped = textBuffer + .replace(/&/g, '&') + .replace(//g, '>'); + result.push(`${escaped}`); + textBuffer = ''; + } + } while (i < coloredString.length) { if (coloredString[i] === '\\' && i + 1 < coloredString.length) { @@ -716,33 +791,34 @@ Uses ONLY ATProto APIs - no aesthetic.computer backend dependencies j++; } if (j < coloredString.length) { - const colorName = coloredString.substring(i + 1, j); - // Check if it's a color code - if (colorMap[colorName.toLowerCase()]) { - currentColor = colorMap[colorName.toLowerCase()]; - } else if (/^\d+,\d+,\d+(,\d+)?$/.test(colorName)) { - // RGB or RGBA value like "255,20,147" or "255,255,255,0" - currentColor = `rgb(${colorName})`; + const colorCode = coloredString.substring(i + 1, j); + + // Flush any buffered text before changing color + flushBuffer(); + + // Parse the color code + if (colorMap[colorCode.toLowerCase()]) { + currentColor = colorMap[colorCode.toLowerCase()]; + } else if (/^\d+,\d+,\d+(,[\d.]+)?$/.test(colorCode)) { + // RGB or RGBA value like "255,20,147" or "255,255,255,0.5" + currentColor = `rgb(${colorCode})`; + } else { + // Unknown color code - try as CSS color name + currentColor = colorCode; } i = j + 1; continue; } } - // Regular character - add with current color - const char = coloredString[i]; - if (char === '<') { - result.push(`<`); - } else if (char === '>') { - result.push(`>`); - } else if (char === '&') { - result.push(`&`); - } else { - result.push(`${char}`); - } + // Regular character - add to buffer + textBuffer += coloredString[i]; i++; } + // Flush any remaining text + flushBuffer(); + return result.join(''); } @@ -776,8 +852,11 @@ Uses ONLY ATProto APIs - no aesthetic.computer backend dependencies if (module && module.KidLisp) { // Create a kidlisp instance for syntax highlighting const kid = new module.KidLisp(); - kid.syntaxHighlightSource = source; + kid.initializeSyntaxHighlighting(source); const coloredString = kid.buildColoredKidlispString(); + + console.log('đ¨ KidLisp colored string sample:', coloredString.substring(0, 100)); + const result = parseColoredKidlisp(coloredString); // Cache the result @@ -884,32 +963,209 @@ Uses ONLY ATProto APIs - no aesthetic.computer backend dependencies }); } - function openSourceModal(uri, source, slug) { - // Create a formatted HTML view of the source code + // Simple JavaScript syntax highlighter + function highlightJavaScript(code) { + // Escape HTML first + const escaped = code.replace(/&/g, '&').replace(//g, '>'); + + // Parse and highlight in order of precedence to avoid conflicts + let result = ''; + let i = 0; + + while (i < escaped.length) { + let matched = false; + + // Multi-line comments + if (escaped.substr(i, 2) === '/*') { + let end = escaped.indexOf('*/', i + 2); + if (end === -1) end = escaped.length; + else end += 2; + result += '' + escaped.substring(i, end) + ''; + i = end; + continue; + } + + // Single-line comments + if (escaped.substr(i, 2) === '//') { + let end = escaped.indexOf('\n', i); + if (end === -1) end = escaped.length; + else end += 1; + result += '' + escaped.substring(i, end) + ''; + i = end; + continue; + } + + // Strings + if (escaped[i] === '"' || escaped[i] === "'" || escaped[i] === '`') { + const quote = escaped[i]; + let end = i + 1; + while (end < escaped.length && escaped[end] !== quote) { + if (escaped[end] === '\\') end += 2; + else end++; + } + end++; // Include closing quote + result += '' + escaped.substring(i, end) + ''; + i = end; + continue; + } + + // Numbers + if (/\d/.test(escaped[i])) { + let end = i; + while (end < escaped.length && /[\d.]/.test(escaped[end])) end++; + result += '' + escaped.substring(i, end) + ''; + i = end; + continue; + } + + // Keywords and identifiers + if (/[a-zA-Z_$]/.test(escaped[i])) { + let end = i; + while (end < escaped.length && /[a-zA-Z0-9_$]/.test(escaped[end])) end++; + const word = escaped.substring(i, end); + + const keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'async', 'await', 'class', 'extends', 'import', 'export', 'from', 'default', 'new', 'try', 'catch', 'finally', 'throw', 'typeof', 'instanceof', 'delete', 'in', 'of', 'void', 'yield', 'static', 'get', 'set', 'super', 'this']; + + if (keywords.includes(word)) { + result += '' + word + ''; + } else { + // Check if it's a function call (followed by opening paren) + let j = end; + while (j < escaped.length && /\s/.test(escaped[j])) j++; + if (escaped[j] === '(') { + result += '' + word + ''; + } else { + result += word; + } + } + i = end; + continue; + } + + // Default: just add the character + result += escaped[i]; + i++; + } + + return result; + } + + // KidLisp syntax highlighter - simple fallback + function highlightKidLispSimple(code) { + // Escape HTML first + code = code.replace(//g, '>'); + + // Comments (semicolons) + code = code.replace(/(;.*$)/gm, + '$1'); + + // Strings + code = code.replace(/("(?:\\.|[^"\\])*")/g, + '$1'); + + // Numbers + code = code.replace(/\b(-?\d+\.?\d*)\b/g, + '$1'); + + // Common KidLisp forms/functions (after opening paren) + code = code.replace(/\(([a-zA-Z_][a-zA-Z0-9_-]*)/g, + '($1'); + + // Parentheses + code = code.replace(/([()])/g, + '$1'); + + return code; + } + + async function openSourceModal(uri, source, slug) { + // Detect file type from slug + const isJavaScript = slug && slug.endsWith('.mjs'); + const isKidLisp = slug && slug.endsWith('.lisp'); + + // Apply syntax highlighting + let highlightedSource; + if (isJavaScript) { + highlightedSource = highlightJavaScript(source); + } else if (isKidLisp) { + // Use the proper KidLisp highlighter with color codes + highlightedSource = await highlightKidlisp(source); + if (!highlightedSource || highlightedSource.trim() === '') { + // Fallback to simple highlighter if the full one fails + highlightedSource = highlightKidLispSimple(source); + } + } else { + highlightedSource = source.replace(//g, '>'); + } + + // Create a formatted HTML view of the source code with animated scrolling const htmlContent = `
-â open on aesthetic.computer
` : ''} -${source.replace(//g, '>')}
+ â open on aesthetic.computer
` : ''} +${highlightedSource}
+