Last active
March 23, 2023 20:50
-
-
Save haxiomic/b2b66a21d4ae26a37104b9416b67485c to your computer and use it in GitHub Desktop.
BodgeShaderEditor. Many of the WebGL shader editor tooling out there is broken or doesn't support WebGL2 and interesting setups like wasm. I created this bodge in a few hours to help solve an issue. See comments for usage. Feel free to do what you like with the code (please build a real shader editor <3)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* BodgeShaderEditor | |
* | |
* Many of the WebGL shader editor tooling out there is broken or doesn't support WebGL2 and interesting setups like wasm. | |
* I created this bodge in a few hours to help solve an issue | |
* | |
* @author haxiomic | |
*/ | |
// replace getContext with our own function | |
let _getContext = HTMLCanvasElement.prototype.getContext; | |
HTMLCanvasElement.prototype.getContext = function (type, ...args) { | |
let ctx = _getContext.apply(this, [type, ...args]); | |
// check if type includes 'webgl' | |
if (type.includes('webgl') && ctx) { | |
let gl = ctx; | |
// check if __shaderEditorEnabled is set | |
if (!gl.__shaderEditorEnabled) { | |
setupWebGLIntercept(gl); | |
setupEditor(gl); | |
} | |
} | |
return ctx; | |
}; | |
function setupShaderEditor(canvas) { | |
if (typeof window === "undefined") { | |
return; | |
} | |
// patch into canvas.getContext to intercept the WebGL context as soon as it's created | |
let _getContext = canvas.getContext; | |
canvas.getContext = function () { | |
let gl = _getContext.apply(this, arguments); | |
console.log('getContext', gl, arguments); | |
if (gl) { | |
// check if __shaderEditorEnabled is set | |
if (!gl.__shaderEditorEnabled) { | |
setupWebGLIntercept(gl); | |
setupEditor(gl); | |
} | |
} | |
return gl; | |
}; | |
} | |
const patchFunction = (obj, functionName, replacement) => { | |
/** @type {Function} */ | |
let original = obj[functionName]; | |
obj[functionName] = function () { | |
// call replacement with first argument being the original function | |
let ret = replacement.apply(this, [original.bind(obj)].concat(Array.from(arguments))); | |
return ret; | |
}; | |
}; | |
let shaderSourceMap = new Map(); | |
let programsMap = new Map(); | |
let replacedPrograms = new Map(); | |
let replacedUniformLocations = new Map(); | |
window.shadersMap = shaderSourceMap; | |
window.programsMap = programsMap; | |
let onProgramAttachShaderCallbacks = []; | |
let onProgramAttachShader = (program, shader) => { | |
let shaderSource = shaderSourceMap.get(shader); | |
onProgramAttachShaderCallbacks.forEach(cb => cb(program, shader, shaderSource)); | |
} | |
let onProgramAddedCallbacks = []; | |
let onProgramAdded = (program) => { | |
onProgramAddedCallbacks.forEach(cb => cb(program, programsMap)); | |
} | |
/** | |
* | |
* @param {WebGL2RenderingContext} gl | |
*/ | |
function setupWebGLIntercept(gl) { | |
gl.__shaderEditorEnabled = true; | |
console.log('setupWebGLIntercept', gl); | |
patchFunction(gl, 'shaderSource', (original, shader, source) => { | |
shaderSourceMap.set(shader, source); | |
// console.log(this, shader, source); | |
return original(shader, source); | |
}); | |
patchFunction(gl, 'attachShader', (original, program, shader) => { | |
let programShaders = programsMap.get(program); | |
if (!programShaders) { | |
programShaders = new Set(); | |
programsMap.set(program, programShaders); | |
onProgramAdded(program); | |
} | |
programShaders.add(shader); | |
onProgramAttachShader(program, shader); | |
return original(program, shader); | |
}); | |
// patch useProgram to switch to replacement program if it exists | |
patchFunction(gl, 'useProgram', (original, program) => { | |
let replacement = replacedPrograms.get(program); | |
if (replacement) { | |
program = replacement; | |
} | |
return original(program); | |
}); | |
// patch all uniform functions to switch to replacement program if it exists | |
['uniform1f', 'uniform2f', 'uniform3f', 'uniform4f', 'uniform1i', 'uniform2i', 'uniform3i', 'uniform4i', 'uniform1fv', 'uniform2fv', 'uniform3fv', 'uniform4fv', 'uniform1iv', 'uniform2iv', 'uniform3iv', 'uniform4iv', 'uniformMatrix2fv', 'uniformMatrix3fv', 'uniformMatrix4fv', 'uniformMatrix2x3fv', 'uniformMatrix3x2fv', 'uniformMatrix2x4fv', 'uniformMatrix4x2fv', 'uniformMatrix3x4fv', 'uniformMatrix4x3fv'].forEach(uniformFunctionName => { | |
patchFunction(gl, uniformFunctionName, (original, location, ...args) => { | |
let program = gl.getParameter(gl.CURRENT_PROGRAM); | |
let replacement = replacedPrograms.get(program); | |
if (replacement) { | |
program = replacement; | |
// check if we've already replaced this uniform location | |
if (replacedUniformLocations.has(location)) { | |
location = replacedUniformLocations.get(location); | |
} else { | |
console.log('replacing uniform location', location, gl.getActiveUniform(program, location.index).name); | |
let newLocation = gl.getUniformLocation(program, gl.getActiveUniform(program, location.index).name); | |
// cache the uniform location so we don't have to look it up again | |
replacedUniformLocations.set(location, newLocation); | |
location = newLocation; | |
} | |
} | |
return original(location, ...args); | |
}); | |
}); | |
// patch attribute functions to switch to replacement program if it exists | |
['vertexAttrib1f', 'vertexAttrib2f', 'vertexAttrib3f', 'vertexAttrib4f', 'vertexAttrib1fv', 'vertexAttrib2fv', 'vertexAttrib3fv', 'vertexAttrib4fv'].forEach(attributeFunctionName => { | |
patchFunction(gl, attributeFunctionName, (original, index, ...args) => { | |
let program = gl.getParameter(gl.CURRENT_PROGRAM); | |
let replacement = replacedPrograms.get(program); | |
if (replacement) { | |
program = replacement; | |
console.log('replacing attribute location', index, gl.getActiveAttrib(program, index).name); | |
index = gl.getAttribLocation(program, gl.getActiveAttrib(program, index).name); | |
} | |
return original(index, ...args); | |
}); | |
}); | |
patchFunction(gl, 'enableExtension', (original, name) => { | |
console.log('enableExtension', name); | |
return original(name); | |
}); | |
// log all texture functions | |
[ | |
// 'texImage2D', | |
'texSubImage2D', | |
'compressedTexImage2D', | |
'compressedTexSubImage2D', | |
'texStorage2D', | |
'texStorage3D', | |
'texImage3D', | |
'texSubImage3D', | |
'copyTexSubImage3D', | |
'compressedTexImage3D', | |
'compressedTexSubImage3D', | |
// set parameter functions | |
'texParameterf', | |
'texParameteri', | |
'texParameterfv', | |
'texParameteriv', | |
].forEach(textureFunctionName => { | |
patchFunction(gl, textureFunctionName, (original, ...args) => { | |
let stack = new Error().stack; | |
console.log(textureFunctionName, args.map( | |
(arg) => { | |
if (typeof arg === 'number') { | |
let constantName = getGLConstantName(arg); | |
if (constantName != null) { | |
return `${arg} (${constantName})`; | |
} else { | |
return arg; | |
} | |
} else { | |
return arg; | |
} | |
} | |
), | |
stack); | |
return original(...args); | |
}); | |
}); | |
} | |
let programIdCounter = 0; | |
let shaderIdCounter = 0; | |
function onShader(gl, editor, programHandle, shaderHandle, shaderSource) { | |
let programId = programHandle.__programId ?? (programHandle.__programId = programIdCounter++); | |
let shaderId = shaderHandle.__shaderId ?? (shaderHandle.__shaderId = shaderIdCounter++); | |
let shaderType = gl.getShaderParameter(shaderHandle, gl.SHADER_TYPE); | |
// shaderType to string | |
let shaderTypeName = shaderType === gl.VERTEX_SHADER ? 'vertex' : 'fragment'; | |
let filename = `program${programId}/${shaderTypeName}.glsl`; | |
let fileBrowser = editor.fileBrowser; | |
if (!fileBrowser.hasFile(filename)) { | |
// create a monaco model for the shader and add it to the file browser | |
let model = monaco.editor.createModel(shaderSource, "glsl", monaco.Uri.parse(`inmemory://shader-folder/${filename}`)); | |
fileBrowser.addFile(filename, model); | |
// when the shader is changed, update the shader source in the shaders map | |
model.onDidChangeContent(() => { | |
let newSource = model.getValue(); | |
// create a new shader from the new source | |
const newShader = gl.createShader(shaderType); | |
newShader.__shaderId = shaderId; | |
gl.shaderSource(newShader, newSource); | |
gl.compileShader(newShader); | |
if (!gl.getShaderParameter(newShader, gl.COMPILE_STATUS)) { | |
console.error("An error occurred compiling the shaders: " + gl.getShaderInfoLog(newShader)); | |
gl.deleteShader(newShader); | |
return; | |
} | |
let otherShader = gl.getAttachedShaders(programHandle).find(s => s !== shaderHandle); | |
const newProgram = gl.createProgram(); | |
newProgram.__programId = programId; | |
gl.attachShader(newProgram, newShader); | |
gl.attachShader(newProgram, otherShader); | |
gl.linkProgram(newProgram); | |
if (!gl.getProgramParameter(newProgram, gl.LINK_STATUS)) { | |
console.error("Unable to initialize the shader program: " + gl.getProgramInfoLog(newProgram)); | |
gl.deleteProgram(newProgram); | |
return; | |
} | |
let currentProgram = gl.getParameter(gl.CURRENT_PROGRAM); | |
// read all uniform values from the old program and set them on the new program | |
gl.useProgram(newProgram); | |
const numUniforms = gl.getProgramParameter(programHandle, gl.ACTIVE_UNIFORMS); | |
for (let i = 0; i < numUniforms; i++) { | |
const uniformInfo = gl.getActiveUniform(programHandle, i); | |
const oldLocation = gl.getUniformLocation(programHandle, uniformInfo.name); | |
const newLocation = gl.getUniformLocation(newProgram, uniformInfo.name); | |
if (oldLocation === null || newLocation === null) { | |
continue; | |
} | |
// console.log('uniform', uniformInfo.name, uniformInfo.type, oldLocation, newLocation); | |
switch (uniformInfo.type) { | |
case gl.FLOAT: | |
gl.uniform1f(newLocation, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.FLOAT_VEC2: | |
gl.uniform2fv(newLocation, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.FLOAT_VEC3: | |
gl.uniform3fv(newLocation, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.FLOAT_VEC4: | |
gl.uniform4fv(newLocation, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.INT: | |
gl.uniform1i(newLocation, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.INT_VEC2: | |
gl.uniform2iv(newLocation, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.INT_VEC3: | |
gl.uniform3iv(newLocation, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.INT_VEC4: | |
gl.uniform4iv(newLocation, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.BOOL: | |
gl.uniform1i(newLocation, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.BOOL_VEC2: | |
gl.uniform2iv(newLocation, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.BOOL_VEC3: | |
gl.uniform3iv(newLocation, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.BOOL_VEC4: | |
gl.uniform4iv(newLocation, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.FLOAT_MAT2: | |
gl.uniformMatrix2fv(newLocation, false, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.FLOAT_MAT3: | |
gl.uniformMatrix3fv(newLocation, false, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.FLOAT_MAT4: | |
gl.uniformMatrix4fv(newLocation, false, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.FLOAT_MAT2x3: | |
gl.uniformMatrix2x3fv(newLocation, false, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.FLOAT_MAT2x4: | |
gl.uniformMatrix2x4fv(newLocation, false, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.FLOAT_MAT3x2: | |
gl.uniformMatrix3x2fv(newLocation, false, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.FLOAT_MAT3x4: | |
gl.uniformMatrix3x4fv(newLocation, false, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.FLOAT_MAT4x2: | |
gl.uniformMatrix4x2fv(newLocation, false, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.FLOAT_MAT4x3: | |
gl.uniformMatrix4x3fv(newLocation, false, gl.getUniform(programHandle, oldLocation)); | |
break; | |
case gl.SAMPLER_2D: | |
case gl.SAMPLER_3D: | |
case gl.SAMPLER_CUBE: | |
case gl.SAMPLER_2D_SHADOW: | |
case gl.SAMPLER_2D_ARRAY: | |
case gl.SAMPLER_2D_ARRAY_SHADOW: | |
case gl.SAMPLER_CUBE_SHADOW: | |
case gl.SAMPLER_3D_SHADOW: | |
case gl.SAMPLER_2D_MULTISAMPLE: | |
case gl.SAMPLER_2D_MULTISAMPLE_ARRAY: | |
case gl.INT_SAMPLER_2D: | |
case gl.INT_SAMPLER_3D: | |
case gl.INT_SAMPLER_CUBE: | |
case gl.INT_SAMPLER_2D_ARRAY: | |
case gl.INT_SAMPLER_2D_MULTISAMPLE: | |
case gl.INT_SAMPLER_2D_MULTISAMPLE_ARRAY: | |
case gl.UNSIGNED_INT_SAMPLER_2D: | |
case gl.UNSIGNED_INT_SAMPLER_3D: | |
case gl.UNSIGNED_INT_SAMPLER_CUBE: | |
case gl.UNSIGNED_INT_SAMPLER_2D_ARRAY: | |
case gl.UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE: | |
case gl.UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: | |
const textureUnit = gl.getUniform(programHandle, oldLocation); | |
gl.uniform1i(newLocation, textureUnit); | |
break; | |
} | |
} | |
// read uniform block bindings from the old program and set them on the new program | |
const numUniformBlocks = gl.getProgramParameter(programHandle, gl.ACTIVE_UNIFORM_BLOCKS); | |
for (let i = 0; i < numUniformBlocks; i++) { | |
const blockInfo = gl.getActiveUniformBlockName(programHandle, i); | |
const blockIndexOld = gl.getUniformBlockIndex(programHandle, blockInfo); | |
const blockIndexNew = gl.getUniformBlockIndex(newProgram, blockInfo); | |
const blockBinding = gl.getActiveUniformBlockParameter(programHandle, blockIndexOld, gl.UNIFORM_BLOCK_BINDING); | |
// console.log('uniform block', blockInfo, blockIndexOld, blockIndexNew, blockBinding); | |
gl.uniformBlockBinding(newProgram, blockIndexNew, blockBinding); | |
} | |
// NOTE: Attribute locations are defined at the shader level, not the program level. | |
// If you use the same attribute names and types, the locations should remain the same. | |
// However, if you still want to ensure that the attribute locations match, | |
// you can use glBindAttribLocation before linking the new program. | |
// | |
// Keep in mind that this method is not necessary for WebGL 2 since attribute | |
// locations can be explicitly specified in the vertex shader using the 'location' layout qualifier. | |
// restore the current program | |
gl.useProgram(currentProgram); | |
// console.log('newProgram', newProgram, shaderType, newShader, otherShader); | |
replacedPrograms.set(programHandle, newProgram); | |
}); | |
} | |
} | |
function createFileBrowser(editor, container) { | |
const fileBrowser = document.createElement('ul'); | |
container.appendChild(fileBrowser); | |
fileBrowser.style.position = 'absolute'; | |
fileBrowser.style.left = '0'; | |
fileBrowser.style.top = '0'; | |
fileBrowser.style.transform = 'translateX(-100%)'; | |
fileBrowser.style.listStyleType = 'none'; | |
fileBrowser.style.padding = '0'; | |
fileBrowser.style.margin = '0'; | |
fileBrowser.style.backgroundColor = '#1e1e1e'; | |
fileBrowser.style.overflowY = 'auto'; | |
fileBrowser.style.color = 'white'; | |
return { | |
hasFile(filename) { | |
for (let i = 0; i < fileBrowser.children.length; i++) { | |
let child = fileBrowser.children[i]; | |
if (child.textContent === filename) { | |
return true; | |
} | |
} | |
return false; | |
}, | |
addFile(filename, model) { | |
const fileItem = document.createElement('li'); | |
fileBrowser.appendChild(fileItem); | |
fileItem.textContent = filename; | |
fileItem.style.padding = '4px 8px'; | |
fileItem.style.cursor = 'pointer'; | |
fileItem.addEventListener('click', () => { | |
editor.setModel(model); | |
}); | |
return fileItem; | |
}, | |
}; | |
} | |
function onEditorReady(gl, editor) { | |
console.log('onEditorReady', gl, editor); | |
// enumerate all programs and add them to the file browser | |
for (let [programHandle, shaders] of programsMap) { | |
for (let shaderHandle of shaders) { | |
let shaderSource = shaderSourceMap.get(shaderHandle); | |
onShader(gl, editor, programHandle, shaderHandle, shaderSource); | |
} | |
} | |
onProgramAttachShaderCallbacks.push((programHandle, shaderHandle, shaderSource) => { | |
onShader(gl, editor, programHandle, shaderHandle, shaderSource); | |
}); | |
} | |
function getGLConstantName(constant) { | |
for (let key in WebGL2RenderingContext) { | |
if (WebGL2RenderingContext[key] === constant) { | |
return key; | |
} | |
} | |
return null; | |
} | |
let insertedEditorDependencies = false; | |
function setupEditor(gl) { | |
console.log('setupEditor') | |
if (!insertedEditorDependencies) { | |
addScript('https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.30.1/min/vs/loader.js') | |
.then(() => { | |
return new Promise((resolve, reject) => { | |
// load and configure the Monaco Editor | |
window.require.config({ paths: { "vs": "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.30.1/min/vs" } }); | |
window.require(["vs/editor/editor.main"], function () { | |
let shaderEditorContainer = document.createElement('div'); | |
shaderEditorContainer.id = 'shader-editor-container'; | |
document.body.appendChild(shaderEditorContainer); | |
shaderEditorContainer.style.position = 'absolute'; | |
shaderEditorContainer.style.top = '0'; | |
shaderEditorContainer.style.right = '0'; | |
shaderEditorContainer.style.width = '40vw'; | |
shaderEditorContainer.style.height = '100vh'; | |
shaderEditorContainer.style.zIndex = '1000'; | |
// hide and show shader with 'h' key | |
document.addEventListener('keydown', (e) => { | |
if (e.key === 'h' && e.target === document.body) { | |
shaderEditorContainer.style.display = shaderEditorContainer.style.display === 'none' ? 'block' : 'none'; | |
} | |
}); | |
// register glsl language | |
monaco.languages.register({ id: "glsl" }); | |
monaco.languages.setMonarchTokensProvider("glsl", glslLanguage); | |
monaco.languages.setLanguageConfiguration("glsl", glslConf); | |
const editor = monaco.editor.create(shaderEditorContainer, { | |
language: "glsl", | |
theme: "vs-dark", | |
}); | |
window.shaderEditor = editor; | |
const fileBrowser = createFileBrowser(editor, shaderEditorContainer); | |
window.shaderEditorFileBrowser = fileBrowser; | |
editor.fileBrowser = fileBrowser; | |
resolve(editor); | |
}); | |
}); | |
}).then((editor) => onEditorReady(gl, editor)); | |
insertedEditorDependencies = true; | |
} | |
} | |
async function addScript(src) { | |
let head = document.head; | |
// check if there's already a script with this src | |
let existingScript = head.querySelector(`script[src="${src}"]`); | |
if (existingScript) { | |
return Promise.resolve(); | |
} | |
let el = document.createElement('script'); | |
el.src = src; | |
head.appendChild(el); | |
return new Promise((resolve, reject) => { | |
el.onload = resolve; | |
el.onerror = reject; | |
}); | |
} | |
const glslConf = { | |
comments: { | |
lineComment: '//', | |
blockComment: ['/*', '*/'] | |
}, | |
brackets: [ | |
['{', '}'], | |
['[', ']'], | |
['(', ')'] | |
], | |
autoClosingPairs: [ | |
{ open: '[', close: ']' }, | |
{ open: '{', close: '}' }, | |
{ open: '(', close: ')' }, | |
{ open: "'", close: "'", notIn: ['string', 'comment'] }, | |
{ open: '"', close: '"', notIn: ['string'] } | |
], | |
surroundingPairs: [ | |
{ open: '{', close: '}' }, | |
{ open: '[', close: ']' }, | |
{ open: '(', close: ')' }, | |
{ open: '"', close: '"' }, | |
{ open: "'", close: "'" } | |
] | |
} | |
const glslKeywords = [ | |
'const', 'uniform', 'break', 'continue', | |
'do', 'for', 'while', 'if', 'else', 'switch', 'case', 'in', 'out', 'inout', 'true', 'false', | |
'invariant', 'discard', 'return', 'sampler2D', 'samplerCube', 'sampler3D', 'struct', | |
'radians', 'degrees', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'pow', 'sinh', 'cosh', 'tanh', 'asinh', 'acosh', 'atanh', | |
'exp', 'log', 'exp2', 'log2', 'sqrt', 'inversesqrt', 'abs', 'sign', 'floor', 'ceil', 'round', 'roundEven', 'trunc', 'fract', 'mod', 'modf', | |
'min', 'max', 'clamp', 'mix', 'step', 'smoothstep', 'length', 'distance', 'dot', 'cross ', | |
'determinant', 'inverse', 'normalize', 'faceforward', 'reflect', 'refract', 'matrixCompMult', 'outerProduct', 'transpose', 'lessThan ', | |
'lessThanEqual', 'greaterThan', 'greaterThanEqual', 'equal', 'notEqual', 'any', 'all', 'not', 'packUnorm2x16', 'unpackUnorm2x16', 'packSnorm2x16', 'unpackSnorm2x16', 'packHalf2x16', 'unpackHalf2x16', | |
'dFdx', 'dFdy', 'fwidth', 'textureSize', 'texture', 'textureProj', 'textureLod', 'textureGrad', 'texelFetch', 'texelFetchOffset', | |
'textureProjLod', 'textureLodOffset', 'textureGradOffset', 'textureProjLodOffset', 'textureProjGrad', 'intBitsToFloat', 'uintBitsToFloat', 'floatBitsToInt', 'floatBitsToUint', 'isnan', 'isinf', | |
'vec2', 'vec3', 'vec4', 'ivec2', 'ivec3', 'ivec4', 'uvec2', 'uvec3', 'uvec4', 'bvec2', 'bvec3', 'bvec4', | |
'mat2', 'mat3', 'mat2x2', 'mat2x3', 'mat2x4', 'mat3x2', 'mat3x3', 'mat3x4', 'mat4x2', 'mat4x3', 'mat4x4', 'mat4', | |
'float', 'int', 'uint', 'void', 'bool', | |
] | |
const glslLanguage = { | |
tokenPostfix: '.glsl', | |
// Set defaultToken to invalid to see what you do not tokenize yet | |
defaultToken: 'invalid', | |
keywords: glslKeywords, | |
operators: [ | |
'=', | |
'>', | |
'<', | |
'!', | |
'~', | |
'?', | |
':', | |
'==', | |
'<=', | |
'>=', | |
'!=', | |
'&&', | |
'||', | |
'++', | |
'--', | |
'+', | |
'-', | |
'*', | |
'/', | |
'&', | |
'|', | |
'^', | |
'%', | |
'<<', | |
'>>', | |
'>>>', | |
'+=', | |
'-=', | |
'*=', | |
'/=', | |
'&=', | |
'|=', | |
'^=', | |
'%=', | |
'<<=', | |
'>>=', | |
'>>>=' | |
], | |
symbols: /[=><!~?:&|+\-*\/\^%]+/, | |
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/, | |
integersuffix: /([uU](ll|LL|l|L)|(ll|LL|l|L)?[uU]?)/, | |
floatsuffix: /[fFlL]?/, | |
encoding: /u|u8|U|L/, | |
tokenizer: { | |
root: [ | |
// identifiers and keywords | |
[ | |
/[a-zA-Z_]\w*/, | |
{ | |
cases: { | |
'@keywords': { token: 'keyword.$0' }, | |
'@default': 'identifier' | |
} | |
} | |
], | |
// Preprocessor directive (#define) | |
[/^\s*#\s*\w+/, 'keyword.directive'], | |
// whitespace | |
{ include: '@whitespace' }, | |
// delimiters and operators | |
[/[{}()\[\]]/, '@brackets'], | |
[/@symbols/, { | |
cases: { | |
'@operators': 'operator', | |
'@default': '' | |
} | |
}], | |
// numbers | |
[/\d*\d+[eE]([\-+]?\d+)?(@floatsuffix)/, 'number.float'], | |
[/\d*\.\d+([eE][\-+]?\d+)?(@floatsuffix)/, 'number.float'], | |
[/0[xX][0-9a-fA-F']*[0-9a-fA-F](@integersuffix)/, 'number.hex'], | |
[/0[0-7']*[0-7](@integersuffix)/, 'number.octal'], | |
[/0[bB][0-1']*[0-1](@integersuffix)/, 'number.binary'], | |
[/\d[\d']*\d(@integersuffix)/, 'number'], | |
[/\d(@integersuffix)/, 'number'], | |
// delimiter: after number because of .\d floats | |
[/[;,.]/, 'delimiter'] | |
], | |
comment: [ | |
[/[^\/*]+/, 'comment'], | |
[/\/\*/, 'comment', '@push'], | |
['\\*/', 'comment', '@pop'], | |
[/[\/*]/, 'comment'] | |
], | |
// Does it have strings? | |
string: [ | |
[/[^\\"]+/, 'string'], | |
[/@escapes/, 'string.escape'], | |
[/\\./, 'string.escape.invalid'], | |
[/"/, { | |
token: 'string.quote', | |
bracket: '@close', | |
next: '@pop' | |
}] | |
], | |
whitespace: [ | |
[/[ \t\r\n]+/, 'white'], | |
[/\/\*/, 'comment', '@comment'], | |
[/\/\/.*$/, 'comment'] | |
] | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage
Must be called before
canvas.getContext()
is used