rampensau
Version:
Color ramp generator using curves within the HSL color model
450 lines (419 loc) • 13.8 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>RampenSau Syntax Highlighter Demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, sans-serif;
background: #18181a;
color: #f8f8f2;
min-height: 100vh;
}
h1 {
text-align: center;
}
#syntax-container {
transition: background-color 0.3s ease;
}
button {
font-size: 1rem;
border-radius: 0.375rem;
transition: all 0.2s ease;
display: block;
width: 100%;
margin-bottom: 2rem;
padding: 0.5rem 1.5rem;
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-weight: 600;
}
button:hover {
opacity: 0.9;
}
.code-block {
transition: background-color 0.3s ease;
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 2rem;
}
pre {
margin: 0;
line-height: 1.5;
font-family: monospace;
overflow: auto;
}
h3 {
margin-bottom: 1rem;
margin-top: 0;
font-weight: 300;
}
.wrapper {
max-width: 64rem;
margin: 0 auto;
padding: 2rem;
}
</style>
</head>
<body>
<div id="syntax-container"></div>
<script type="module">
import {
generateColorRamp,
generateColorRampWithCurve,
generateColorRampParams,
colorUtils,
utils,
} from "./index.mjs";
const { uniqueRandomHues, colorHarmonies, colorToCSS, harveyHue } =
colorUtils;
const { scaleSpreadArray, shuffleArray } = utils;
const random = (min, max) => Math.random() * (max - min) + min;
function getRampensauColors(isLightMode = false, colorCount = 24) {
return generateColorRamp({
total: colorCount,
hCycles: random(0.1, 2),
hStartCenter: 0.5,
sRange: [random(0.9, 0.7), random(0.5, 0.3)],
lRange: isLightMode
? [random(0.85, 1), random(0, 0.15)]
: [random(0, 0.15), random(0.8, 0.99)],
lEasing: isLightMode
? (x) => -(Math.cos(Math.PI * x) - 1) / 2
: (x, fr) => Math.pow(x, 1.5),
}).map((hsl) => colorToCSS(hsl, "hsl"));
}
class SyntaxHighlighter {
constructor() {
this.colors = [];
// Semantic color indices - organize by purpose
this.colorMap = {
// Background and UI elements
background: 2,
codeBackground: 0,
buttonBackground: 3,
// Syntax elements
comment: 5,
string: random(-20, -15),
space: 15,
keyword: -16,
number: -14,
function: -12,
method: -10,
property: -9,
operator: -7,
builtin: -5,
variable: -3,
default: -1,
// Always use the last color for text
text: -1,
};
// Language keywords and syntax definitions
this.keywords = [
"class",
"def",
"return",
"for",
"in",
"if",
"const",
"let",
"var",
"async",
"await",
"try",
"catch",
"throw",
"new",
"function",
"typeof",
"instanceof",
"this",
"super",
"extends",
"static",
];
this.builtins = [
"console",
"Math",
"Object",
"Array",
"String",
"Number",
"Promise",
"Date",
"RegExp",
"Map",
"Set",
"JSON",
"Error",
];
this.operators = [
"+",
"-",
"*",
"/",
"%",
"=",
"==",
"===",
"!=",
"!==",
">",
"<",
">=",
"<=",
"&&",
"||",
"!",
"??",
"?.",
];
}
tokenizeLine(line) {
const tokens = [];
let current = 0;
let buffer = "";
const pushBuffer = (type = "default") => {
if (buffer) {
if (this.keywords.includes(buffer)) {
tokens.push({ type: "keyword", content: buffer });
} else if (this.builtins.includes(buffer)) {
tokens.push({ type: "builtin", content: buffer });
} else {
const nextChar = line[current];
if (nextChar === "(") {
tokens.push({ type: "function", content: buffer });
} else if (nextChar === ".") {
tokens.push({ type: "property", content: buffer });
} else {
tokens.push({ type, content: buffer });
}
}
buffer = "";
}
};
while (current < line.length) {
if (line[current] === '"' || line[current] === "'") {
pushBuffer();
const quote = line[current];
let string = quote;
current++;
while (current < line.length && line[current] !== quote) {
string += line[current];
current++;
}
if (current < line.length) {
string += quote;
current++;
}
tokens.push({ type: "string", content: string });
continue;
}
if (line[current] === "/" && line[current + 1] === "/") {
pushBuffer();
tokens.push({ type: "comment", content: line.slice(current) });
break;
}
const possibleOperator = this.operators.find((op) =>
line.slice(current).startsWith(op)
);
if (possibleOperator) {
pushBuffer();
tokens.push({ type: "operator", content: possibleOperator });
current += possibleOperator.length;
continue;
}
if (line[current] === ".") {
pushBuffer();
tokens.push({ type: "operator", content: "." });
current++;
while (current < line.length && /[\w$_]/.test(line[current])) {
buffer += line[current];
current++;
}
if (buffer) {
tokens.push({ type: "method", content: buffer });
buffer = "";
}
continue;
}
if (/[0-9]/.test(line[current])) {
pushBuffer();
let number = "";
while (current < line.length && /[0-9.]/.test(line[current])) {
number += line[current];
current++;
}
tokens.push({ type: "number", content: number });
continue;
}
if (/\s/.test(line[current])) {
pushBuffer();
let spaces = "";
while (current < line.length && /\s/.test(line[current])) {
spaces += line[current] === " " ? "·" : line[current];
current++;
}
tokens.push({ type: "space", content: spaces });
continue;
}
if (/[\w$_]/.test(line[current])) {
buffer += line[current];
current++;
continue;
}
pushBuffer();
tokens.push({ type: "operator", content: line[current] });
current++;
}
pushBuffer();
return tokens;
}
createTokenSpan(token) {
const span = document.createElement("span");
span.textContent = token.content;
let colorIndex = this.colorMap[token.type];
if (typeof colorIndex === "undefined")
colorIndex = this.colorMap.default;
// Support negative indices for end of array
const idx =
colorIndex < 0 ? this.colors.length + colorIndex : colorIndex;
span.style.color =
this.colors[idx] || this.colors[this.colors.length - 1];
return span;
}
renderLine(line) {
const div = document.createElement("div");
const tokens = this.tokenizeLine(line);
tokens.forEach((token) => {
div.appendChild(this.createTokenSpan(token));
});
return div;
}
createCodeBlocks() {
const codeContainer = document.createElement("div");
codeContainer.style.display = "flex";
codeContainer.style.flexDirection = "column";
codeContainer.style.gap = "2rem";
codeContainer.appendChild(
this.createCodeBlock("JavaScript Example", javascriptCode)
);
codeContainer.appendChild(
this.createCodeBlock("Python Example", pythonCode)
);
return codeContainer;
}
createShuffleButton() {
const shuffleButton = document.createElement("button");
shuffleButton.textContent = "Generate Theme";
shuffleButton.style.background = this.colors[3];
shuffleButton.style.color =
this.colors[this.colors.length - 1] || "#fff";
shuffleButton.addEventListener("click", () => {
this.shuffle();
this.updateColors();
});
return shuffleButton;
}
applyContainerStyles(container) {
container.style.background = this.colors[2];
container.style.color = this.colors[this.colors.length - 1];
return container;
}
render(container) {
container.innerHTML = "";
container.className = "";
// Apply base styles
this.applyContainerStyles(container);
// Create and style wrapper
const wrapper = document.createElement("div");
wrapper.className = "wrapper";
wrapper.style.color = this.colors[this.colors.length - 1];
const title = document.createElement("h1");
title.textContent = "RampenSau Syntax Highlighter";
title.style.color = "currentColor";
// Add button and code blocks
wrapper.appendChild(title);
wrapper.appendChild(this.createShuffleButton());
wrapper.appendChild(this.createCodeBlocks());
// Add everything to container
container.appendChild(wrapper);
}
createCodeBlock(title, code) {
const block = document.createElement("div");
block.classList.add("code-block");
block.style.background = this.colors[0];
block.style.color = this.colors[this.colors.length - 1];
const heading = document.createElement("h3");
heading.textContent = title;
heading.style.color = this.colors[this.colors.length - 1];
block.appendChild(heading);
const pre = document.createElement("pre");
pre.style.color = this.colors[this.colors.length - 1];
code.split("\n").forEach((line) => {
pre.appendChild(this.renderLine(line));
});
block.appendChild(pre);
return block;
}
shuffle() {
this.colors = getRampensauColors(random(0, 1) < 0.5, 40);
}
updateColors() {
const container = document.querySelector("#syntax-container");
this.applyContainerStyles(container);
const wrapper = container.querySelector(".wrapper");
wrapper.style.color = this.colors[this.colors.length - 1];
// Update button styles
const button = wrapper.querySelector("button");
button.style.background = this.colors[3];
button.style.color = this.colors[this.colors.length - 1];
// Replace code container with new one
const oldCodeContainer = wrapper.querySelector("div");
const newCodeContainer = this.createCodeBlocks();
wrapper.replaceChild(newCodeContainer, oldCodeContainer);
}
}
const pythonCode = `class DataProcessor:
def __init__(self, data: List[Dict]):
self.data = data
self._processed = False
def process(self) -> None:
"""Process the data and update internal state."""
for item in self.data:
if item['status'] == 'active':
self._transform(item)
self._processed = True
@property
def is_processed(self) -> bool:
return self._processed`;
const javascriptCode = `const fetchUserData = async (userId) => {
try {
const response = await api.get(${"`/users/${userId}`"});
const { data } = response;
if (!data.isActive) {
throw new Error('User account inactive');
}
return {
...data,
lastLogin: new Date(data.lastLogin)
};
} catch (error) {
console.error('Failed to fetch user:', error);
return null;
}
};`;
// Mount highlighter
const container = document.getElementById("syntax-container");
const highlighter = new SyntaxHighlighter();
highlighter.shuffle();
highlighter.render(container);
</script>
</body>
</html>