(function(){
window.syntax = {
highlightAllCodeBlocks
};
const BUILTINS = [
'map','eq','plus','sput','slog','sum','inc','keys','tap','upper','lower',
'multiply','prop','assoc','merge','select','cat','comp','range','filter',
'reduce','pluck','head','tail','take','some','none','flatten','push','fall',
'every','find','group','intersect','sort','union','each','length'
];
const KEYWORDS = ['fn'];
function highlightAllCodeBlocks() {
const blocks = document.querySelectorAll('.code-block');
blocks.forEach(block => {
const originalText = block.textContent;
const tokens = parseToTokens(originalText);
const html = tokensToHtml(tokens);
block.innerHTML = html;
});
}
function parseToTokens(code) {
const tokens = [];
let i = 0;
const len = code.length;
let state = 'NORMAL';
let currentText = '';
let bracketStack = [];
function pushToken(type) {
if (currentText.length > 0) {
tokens.push({ type, text: currentText });
currentText = '';
}
}
function pushBracketToken(ch, isOpen) {
tokens.push({ type: 'bracket', text: ch, bracketType: ch, isOpen });
}
while (i < len) {
const c = code[i];
const next = (i + 1 < len) ? code[i+1] : '';
if (state === 'NORMAL') {
if (c === '/' && next === '/') {
pushToken('normal');
state = 'IN_LINE_COMMENT';
i += 2; continue;
}
if (c === '/' && next === '*') {
pushToken('normal');
state = 'IN_BLOCK_COMMENT';
i += 2;
continue;
}
if (c === '"') {
pushToken('normal');
currentText += c;
state = 'IN_STRING';
i++;
continue;
}
if (c === '(' || c === '{' || c === '[') {
pushToken('normal');
bracketStack.push(c);
pushBracketToken(c, true);
i++;
continue;
}
if (c === ')' || c === '}' || c === ']') {
pushToken('normal');
const top = (bracketStack.length > 0) ? bracketStack[bracketStack.length - 1] : null;
if (matchesBracket(top, c)) {
bracketStack.pop();
}
pushBracketToken(c, false);
i++;
continue;
}
currentText += c;
i++;
}
else if (state === 'IN_STRING') {
currentText += c;
if (c === '"' && (code[i-1] !== '\\' || code[i-1] === '\\' && code[i-2] === '\\')) {
pushToken('string');
state = 'NORMAL';
}
i++;
}
else if (state === 'IN_LINE_COMMENT') {
currentText += c;
if (c === '\n') {
pushToken('comment');
state = 'NORMAL';
}
i++;
}
else if (state === 'IN_BLOCK_COMMENT') {
currentText += c;
if (c === '*' && next === '/') {
currentText += '/';
i += 2;
pushToken('comment');
state = 'NORMAL';
} else {
i++;
}
}
}
if (currentText.length > 0) {
const finalType = (state === 'IN_STRING') ? 'string'
: (state === 'IN_LINE_COMMENT' || state === 'IN_BLOCK_COMMENT') ? 'comment'
: 'normal';
pushToken(finalType);
}
return postProcessTokens(tokens);
}
function matchesBracket(opening, closing) {
if (opening === '(' && closing === ')') return true;
if (opening === '{' && closing === '}') return true;
if (opening === '[' && closing === ']') return true;
return false;
}
function postProcessTokens(tokens) {
const results = [];
for (let t of tokens) {
if (t.type === 'normal') {
const splitted = splitNormalToken(t.text);
results.push(...splitted);
} else {
results.push(t);
}
}
return results;
}
function splitNormalToken(txt) {
const regex = /([A-Za-z_]\w*|\d+(\.\d+)?|[^A-Za-z0-9_\s]+)/g;
let tokens = [];
let lastIndex = 0;
let match;
while ((match = regex.exec(txt)) !== null) {
const before = txt.slice(lastIndex, match.index);
if (before) {
tokens.push({ type: 'normal', text: before });
}
const str = match[0];
if (isNumber(str)) {
tokens.push({ type: 'number', text: str });
} else if (isKeyword(str)) {
tokens.push({ type: 'keyword', text: str });
} else if (isBuiltin(str)) {
tokens.push({ type: 'builtin', text: str });
} else {
tokens.push({ type: 'normal', text: str });
}
lastIndex = regex.lastIndex;
}
if (lastIndex < txt.length) {
const remainder = txt.slice(lastIndex);
tokens.push({ type: 'normal', text: remainder });
}
return tokens;
}
function isNumber(str) {
return /^\d+(\.\d+)?$/.test(str);
}
function isKeyword(str) {
return KEYWORDS.includes(str);
}
function isBuiltin(str) {
return BUILTINS.includes(str);
}
function tokensToHtml(tokens) {
let html = '';
for (let tk of tokens) {
const escaped = escapeHtml(tk.text);
let className = '';
switch (tk.type) {
case 'string': className = 'af-string'; break;
case 'comment': className = 'af-comment'; break;
case 'bracket': className = 'af-bracket'; break;
case 'number': className = 'af-number'; break;
case 'builtin': className = 'af-builtin'; break;
case 'keyword': className = 'af-keyword'; break;
default:
className = 'af-normal';
}
html += `<span class="${className}">${escaped}</span>`;
}
return html;
}
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
}
})();