mirror of
				https://github.com/alecthomas/chroma.git
				synced 2025-10-30 23:57:49 +02:00 
			
		
		
		
	feat: WASM playground
This commit is contained in:
		
							
								
								
									
										18
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -9,19 +9,13 @@ jobs: | ||||
|     name: Test | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout code | ||||
|         uses: actions/checkout@v4 | ||||
|       - name: Init Hermit | ||||
|         run: ./bin/hermit env -r >> $GITHUB_ENV | ||||
|       - name: Test | ||||
|         run: go test ./... | ||||
|       - uses: actions/checkout@v4 | ||||
|       - uses: cashapp/activate-hermit@v1 | ||||
|       - run: go test ./... | ||||
|   lint: | ||||
|     name: Lint | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout code | ||||
|         uses: actions/checkout@v4 | ||||
|       - name: Init Hermit | ||||
|         run: ./bin/hermit env -r >> $GITHUB_ENV | ||||
|       - name: golangci-lint | ||||
|         run: golangci-lint run | ||||
|       - uses: actions/checkout@v4 | ||||
|       - uses: cashapp/activate-hermit@v1 | ||||
|       - run: golangci-lint run | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -23,3 +23,6 @@ _models/ | ||||
| _examples/ | ||||
| *.min.* | ||||
| build/ | ||||
|  | ||||
| cmd/chromad/static/chroma.wasm | ||||
| cmd/chromad/static/wasm_exec.js | ||||
|   | ||||
							
								
								
									
										12
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								Makefile
									
									
									
									
									
								
							| @@ -15,12 +15,20 @@ tokentype_string.go: types.go | ||||
| .PHONY: chromad | ||||
| chromad: build/chromad | ||||
|  | ||||
| build/chromad: $(shell find cmd/chromad -name '*.go' -o -name '*.html' -o -name '*.css' -o -name '*.js') | ||||
| build/chromad: $(shell find cmd/chromad -name '*.go' -o -name '*.html' -o -name '*.css' -o -name '*.js') \ | ||||
| 	cmd/chromad/static/wasm_exec.js \ | ||||
| 	cmd/chromad/static/chroma.wasm | ||||
| 	rm -rf build | ||||
| 	esbuild --bundle cmd/chromad/static/index.js --minify --outfile=cmd/chromad/static/index.min.js | ||||
| 	esbuild --platform=node --bundle cmd/chromad/static/index.js --minify --outfile=cmd/chromad/static/index.min.js | ||||
| 	esbuild --bundle cmd/chromad/static/index.css --minify --outfile=cmd/chromad/static/index.min.css | ||||
| 	(export CGOENABLED=0 ; go build -C cmd/chromad -ldflags="-X 'main.version=$(VERSION)'" -o ../../build/chromad .) | ||||
|  | ||||
| cmd/chromad/static/wasm_exec.js: $(shell tinygo env TINYGOROOT)/targets/wasm_exec.js | ||||
| 	install -m644 $< $@ | ||||
|  | ||||
| cmd/chromad/static/chroma.wasm: cmd/libchromawasm/main.go | ||||
| 	tinygo build -no-debug -target wasm -o $@ $< | ||||
|  | ||||
| upload: build/chromad | ||||
| 	scp build/chromad root@swapoff.org: && \ | ||||
| 		ssh root@swapoff.org 'install -m755 ./chromad /srv/http/swapoff.org/bin && service chromad restart' | ||||
|   | ||||
							
								
								
									
										1
									
								
								bin/.binaryen-123.pkg
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/.binaryen-123.pkg
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| hermit | ||||
							
								
								
									
										1
									
								
								bin/.caddy-2.10.0.pkg
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/.caddy-2.10.0.pkg
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| hermit | ||||
							
								
								
									
										1
									
								
								bin/.tinygo-0.38.0.pkg
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/.tinygo-0.38.0.pkg
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| hermit | ||||
							
								
								
									
										1
									
								
								bin/.typescript-7.0.0-dev.20250629.1.pkg
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/.typescript-7.0.0-dev.20250629.1.pkg
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| hermit | ||||
							
								
								
									
										1
									
								
								bin/binaryen-unittests
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/binaryen-unittests
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| .binaryen-123.pkg | ||||
							
								
								
									
										1
									
								
								bin/tinygo
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/tinygo
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| .tinygo-0.38.0.pkg | ||||
							
								
								
									
										1
									
								
								bin/wasm-as
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/wasm-as
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| .binaryen-123.pkg | ||||
							
								
								
									
										1
									
								
								bin/wasm-ctor-eval
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/wasm-ctor-eval
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| .binaryen-123.pkg | ||||
							
								
								
									
										1
									
								
								bin/wasm-dis
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/wasm-dis
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| .binaryen-123.pkg | ||||
							
								
								
									
										1
									
								
								bin/wasm-emscripten-finalize
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/wasm-emscripten-finalize
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| .binaryen-123.pkg | ||||
							
								
								
									
										1
									
								
								bin/wasm-fuzz-lattices
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/wasm-fuzz-lattices
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| .binaryen-123.pkg | ||||
							
								
								
									
										1
									
								
								bin/wasm-fuzz-types
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/wasm-fuzz-types
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| .binaryen-123.pkg | ||||
							
								
								
									
										1
									
								
								bin/wasm-merge
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/wasm-merge
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| .binaryen-123.pkg | ||||
							
								
								
									
										1
									
								
								bin/wasm-metadce
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/wasm-metadce
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| .binaryen-123.pkg | ||||
							
								
								
									
										1
									
								
								bin/wasm-opt
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/wasm-opt
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| .binaryen-123.pkg | ||||
							
								
								
									
										1
									
								
								bin/wasm-reduce
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/wasm-reduce
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| .binaryen-123.pkg | ||||
							
								
								
									
										1
									
								
								bin/wasm-shell
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/wasm-shell
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| .binaryen-123.pkg | ||||
							
								
								
									
										1
									
								
								bin/wasm-split
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/wasm-split
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| .binaryen-123.pkg | ||||
							
								
								
									
										1
									
								
								bin/wasm2js
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								bin/wasm2js
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| .binaryen-123.pkg | ||||
							
								
								
									
										83
									
								
								cmd/chromad/static/chroma.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								cmd/chromad/static/chroma.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| // chroma.js - TinyGo WASM runtime initialization for Chroma syntax highlighter | ||||
|  | ||||
| // Import wasm_exec.js so that it initialises the Go WASM runtime. | ||||
| import './wasm_exec.js'; | ||||
|  | ||||
| class ChromaWASM { | ||||
|     constructor() { | ||||
|         this.go = null; | ||||
|         this.wasm = null; | ||||
|         this.ready = false; | ||||
|         this.readyPromise = this.init(); | ||||
|     } | ||||
|  | ||||
|     async init() { | ||||
|         try { | ||||
|             // Create a new Go instance | ||||
|             this.go = new Go(); | ||||
|  | ||||
|             // Load the WASM module | ||||
|             const wasmResponse = await fetch('./static/chroma.wasm'); | ||||
|             if (!wasmResponse.ok) { | ||||
|                 throw new Error(`Failed to fetch chroma.wasm: ${wasmResponse.status}`); | ||||
|             } | ||||
|  | ||||
|             const wasmBytes = await wasmResponse.arrayBuffer(); | ||||
|             const wasmModule = await WebAssembly.instantiate(wasmBytes, this.go.importObject); | ||||
|  | ||||
|             this.wasm = wasmModule.instance; | ||||
|  | ||||
|             // Run the Go program | ||||
|             this.go.run(this.wasm); | ||||
|  | ||||
|             this.ready = true; | ||||
|             console.log('Chroma WASM module initialized successfully'); | ||||
|         } catch (error) { | ||||
|             console.error('Failed to initialize Chroma WASM module:', error); | ||||
|             throw error; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async waitForReady() { | ||||
|         await this.readyPromise; | ||||
|         if (!this.ready) { | ||||
|             throw new Error('Chroma WASM module failed to initialize'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async highlight(source, lexer, formatter, withClasses) { | ||||
|         await this.waitForReady(); | ||||
|  | ||||
|         if (typeof window.highlight !== 'function') { | ||||
|             throw new Error('highlight function not available from WASM module'); | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             return window.highlight(source, lexer, formatter, withClasses); | ||||
|         } catch (error) { | ||||
|             console.error('Error calling highlight function:', error); | ||||
|             throw error; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| export function isWasmSupported() { | ||||
|   try { | ||||
|     if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { | ||||
|       // The smallest possible WebAssembly module (magic number + version) | ||||
|       const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)); | ||||
|       if (module instanceof WebAssembly.Module) { | ||||
|         // Try to instantiate the module to ensure it's truly runnable | ||||
|         return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; | ||||
|       } | ||||
|     } | ||||
|   } catch (e) { | ||||
|     // An error occurred (e.g., due to CSP or other restrictions) | ||||
|   } | ||||
|   return false; | ||||
| } | ||||
|  | ||||
|  | ||||
| // Create global instance, null if WASM is not supported. | ||||
| export const chroma = isWasmSupported() ? new ChromaWASM() : null; | ||||
| @@ -1,4 +1,5 @@ | ||||
| import * as Base64 from "./base64.js"; | ||||
| import { chroma } from "./chroma.js"; | ||||
|  | ||||
| document.addEventListener("DOMContentLoaded", function () { | ||||
| 	var darkMode = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches); | ||||
| @@ -22,6 +23,38 @@ document.addEventListener("DOMContentLoaded", function () { | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   async function renderServer(formData) { | ||||
| 		return (await fetch("api/render", { | ||||
| 				  method: 'POST', | ||||
| 					mode: 'cors', | ||||
| 					cache: 'no-cache', | ||||
| 					credentials: 'same-origin', | ||||
| 					headers: { | ||||
| 						'X-CSRF-Token': csrfToken, | ||||
| 						'Content-Type': 'application/json', | ||||
| 					}, | ||||
| 					redirect: 'follow', | ||||
| 					referrer: 'no-referrer', | ||||
| 					body: JSON.stringify(formData), | ||||
| 				})).json(); | ||||
|   } | ||||
|  | ||||
|   async function renderWasm(formData) { | ||||
| 		return await chroma.highlight( | ||||
| 			formData.text, | ||||
| 			formData.language, | ||||
| 			formData.style, | ||||
| 			formData.classes, | ||||
| 		); | ||||
|   } | ||||
|  | ||||
|   async function render(formData) { | ||||
| 		return chroma !== null | ||||
| 			? renderWasm(formData) | ||||
| 			: renderServer(formData); | ||||
|   } | ||||
|  | ||||
|  | ||||
|   // https://stackoverflow.com/a/37697925/7980 | ||||
|   function handleTab(e) { | ||||
|     var after, before, end, lastNewLine, changeLength, re, replace, selection, start, val; | ||||
| @@ -97,38 +130,33 @@ document.addEventListener("DOMContentLoaded", function () { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function update(event) { | ||||
|     fetch("api/render", { | ||||
|       method: 'POST', | ||||
|       mode: 'cors', | ||||
|       cache: 'no-cache', | ||||
|       credentials: 'same-origin', | ||||
|       headers: { | ||||
|         'X-CSRF-Token': csrfToken, | ||||
|         'Content-Type': 'application/json', | ||||
|       }, | ||||
|       redirect: 'follow', | ||||
|       referrer: 'no-referrer', | ||||
|       body: JSON.stringify(getFormJSON()), | ||||
|     }).then(data => { | ||||
|       data.json().then( | ||||
|         value => { | ||||
|           if (value.language) { | ||||
|             languageSelect.value = value.language; | ||||
|           } | ||||
|           style.innerHTML = "#output { " + value.background + "}"; | ||||
|           if (htmlCheckbox.checked) { | ||||
|             output.innerText = value.html; | ||||
|           } else { | ||||
|             output.innerHTML = value.html; | ||||
|           } | ||||
|         } | ||||
|       ); | ||||
|     }).catch(reason => { | ||||
|       console.log(reason); | ||||
|     }); | ||||
|   async function update(event) { | ||||
|     try { | ||||
|       const formData = getFormJSON(); | ||||
|       const value = await render(formData); | ||||
|  | ||||
|     event.preventDefault(); | ||||
|       if (value.language) { | ||||
|         languageSelect.value = value.language; | ||||
|       } | ||||
|       style.innerHTML = "#output { " + value.background + "}"; | ||||
|       if (htmlCheckbox.checked) { | ||||
|         output.innerText = value.html; | ||||
|       } else { | ||||
|         output.innerHTML = value.html; | ||||
|       } | ||||
|     } catch (error) { | ||||
|       console.error('Error highlighting code:', error); | ||||
|       // Fallback: display plain text | ||||
|       if (htmlCheckbox.checked) { | ||||
|         output.innerText = textArea.value; | ||||
|       } else { | ||||
|         output.innerHTML = '<pre>' + textArea.value + '</pre>'; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (event) { | ||||
|       event.preventDefault(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function share(event) { | ||||
| @@ -157,7 +185,7 @@ document.addEventListener("DOMContentLoaded", function () { | ||||
|   } | ||||
|  | ||||
|   var eventHandler = (event) => update(event); | ||||
|   var debouncedEventHandler = debounce(eventHandler, 250); | ||||
|   var debouncedEventHandler = debounce(eventHandler, chroma === null ? 250 : 100); | ||||
|  | ||||
|   languageSelect.addEventListener('change', eventHandler); | ||||
|   styleSelect.addEventListener('change', eventHandler); | ||||
|   | ||||
							
								
								
									
										77
									
								
								cmd/libchromawasm/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								cmd/libchromawasm/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| //go:build wasm | ||||
|  | ||||
| // Package main is an experimental WASM library intended for TinyGO. | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"strings" | ||||
| 	"syscall/js" | ||||
|  | ||||
| 	"github.com/alecthomas/chroma/v2" | ||||
| 	"github.com/alecthomas/chroma/v2/formatters/html" | ||||
| 	"github.com/alecthomas/chroma/v2/lexers" | ||||
| 	"github.com/alecthomas/chroma/v2/styles" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	// Register the highlight function with the JavaScript global object | ||||
| 	js.Global().Set("highlight", js.FuncOf(highlight)) | ||||
|  | ||||
| 	// Keep the program running | ||||
| 	select {} | ||||
| } | ||||
|  | ||||
| // Highlight source code using Chroma. | ||||
| // | ||||
| // Equivalent to the JS function: | ||||
| // | ||||
| //	function highlight(source, lexer, styleName, classes) | ||||
| // | ||||
| // If the "lexer" is unknown, this will attempt to autodetect the language type. | ||||
| func highlight(this js.Value, args []js.Value) any { | ||||
| 	source := args[0].String() | ||||
| 	lexer := args[1].String() | ||||
| 	styleName := args[2].String() | ||||
| 	classes := args[3].Bool() | ||||
|  | ||||
| 	language := lexers.Get(lexer) | ||||
| 	if language == nil { | ||||
| 		language = lexers.Analyse(source) | ||||
| 		if language != nil { | ||||
| 			lexer = language.Config().Name | ||||
| 		} | ||||
| 	} | ||||
| 	if language == nil { | ||||
| 		language = lexers.Fallback | ||||
| 	} | ||||
|  | ||||
| 	tokens, err := chroma.Coalesce(language).Tokenise(nil, source) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	style := styles.Get(styleName) | ||||
| 	if style == nil { | ||||
| 		style = styles.Fallback | ||||
| 	} | ||||
|  | ||||
| 	buf := &strings.Builder{} | ||||
| 	options := []html.Option{} | ||||
| 	if classes { | ||||
| 		options = append(options, html.WithClasses(true), html.Standalone(true)) | ||||
| 	} | ||||
| 	formatter := html.New(options...) | ||||
| 	err = formatter.Format(buf, style, tokens) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	lang := language.Config().Name | ||||
| 	if language == lexers.Fallback { | ||||
| 		lang = "" | ||||
| 	} | ||||
| 	return js.ValueOf(map[string]any{ | ||||
| 		"html":       buf.String(), | ||||
| 		"language":   lang, | ||||
| 		"background": html.StyleEntryToCSS(style.Get(chroma.Background)), | ||||
| 	}) | ||||
| } | ||||
| @@ -7,6 +7,7 @@ | ||||
| 		":semanticCommitScope(deps)", | ||||
| 		"group:allNonMajor", | ||||
| 		"schedule:earlyMondays", // Run once a week. | ||||
| 		'helpers:pinGitHubActionDigests', | ||||
| 	], | ||||
| 	"packageRules": [ | ||||
| 		{ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user