From 34e7cccacda095c563b78693550f7a4b8d6fa857 Mon Sep 17 00:00:00 2001 From: sdegutis Date: Thu, 22 Aug 2024 08:06:31 -0500 Subject: [PATCH] Starting over with new monarch playground. --- site/@imlib/processor.ts | 9 + site/load-samples.tsx | 19 +- site/monarch/index.html | 16 ++ site/monarch/monarch-playground.tsx | 66 +++++ site/monarch/samplecode.tsx | 229 ++++++++++++++++++ site/monarch/style.css | 10 + site/theme.ts | 18 ++ .../{token-provider.tsx => token-provider.ts} | 131 +++++----- 8 files changed, 429 insertions(+), 69 deletions(-) create mode 100644 site/@imlib/processor.ts create mode 100644 site/monarch/index.html create mode 100644 site/monarch/monarch-playground.tsx create mode 100644 site/monarch/samplecode.tsx create mode 100644 site/monarch/style.css create mode 100644 site/theme.ts rename site/{token-provider.tsx => token-provider.ts} (51%) diff --git a/site/@imlib/processor.ts b/site/@imlib/processor.ts new file mode 100644 index 0000000..43dba30 --- /dev/null +++ b/site/@imlib/processor.ts @@ -0,0 +1,9 @@ +import { processSite, SiteProcessor } from "@imlib/core"; + +export default (files => { + const allFiles = [...files]; + const out = processSite(allFiles); + out.set('/token-provider.ts', allFiles.find(f => f.path === '/token-provider.js')!.module!.source); + out.set('/monarch/samplecode.tsx', allFiles.find(f => f.path === '/monarch/samplecode.js')!.module!.source); + return out; +}) as SiteProcessor; diff --git a/site/load-samples.tsx b/site/load-samples.tsx index 2eb0473..f047b64 100644 --- a/site/load-samples.tsx +++ b/site/load-samples.tsx @@ -1,4 +1,6 @@ import monaco from '@imlib/monaco-esm'; +import type monacoTypes from 'monaco-editor'; +import { setupTheme } from './theme.js'; import { tokenProvider } from './token-provider.js'; import { Mod, modules } from './vanillajsx/compiler.js'; @@ -8,22 +10,9 @@ monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ target: monaco.languages.typescript.ScriptTarget.ESNext, }); -monaco.editor.defineTheme('vsc2', { - base: 'vs-dark', - inherit: true, - rules: [ - // { token: "identifier.ts", foreground: "9CDCFE" }, - { token: "property.ts", foreground: "9CDCFE" }, - { token: "function.ts", foreground: "DCDCAA" }, - { token: "method.ts", foreground: "DCDCAA" }, - // { token: "delimiter.ts", foreground: "569CD6" }, - ], - colors: { - "editor.background": '#1b1f25', - }, -}); +setupTheme(); -monaco.languages.setMonarchTokensProvider('typescript', tokenProvider); +monaco.languages.setMonarchTokensProvider('typescript', tokenProvider as monacoTypes.languages.IMonarchLanguage); for (const sample of document.querySelectorAll('.sample')) { const code = sample.querySelector('.sample-code>pre')!.textContent!.trim(); diff --git a/site/monarch/index.html b/site/monarch/index.html new file mode 100644 index 0000000..99a9dd4 --- /dev/null +++ b/site/monarch/index.html @@ -0,0 +1,16 @@ + + + + + + + Monarch test + + + + + +
+ + + \ No newline at end of file diff --git a/site/monarch/monarch-playground.tsx b/site/monarch/monarch-playground.tsx new file mode 100644 index 0000000..299c8fd --- /dev/null +++ b/site/monarch/monarch-playground.tsx @@ -0,0 +1,66 @@ +import monaco from '@imlib/monaco-esm'; +import { setupTheme } from '../theme.js'; + +setupTheme(); + +// monaco.languages.typescript.typescriptDefaults.addExtraLib(jsxlib(), `ts:filename/jsx.d.ts`); +monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ + jsx: monaco.languages.typescript.JsxEmit.ReactNative, + target: monaco.languages.typescript.ScriptTarget.ESNext, +}); + +const root = document.getElementById('root') as HTMLDivElement; + +const file1 = await fetch('/token-provider.ts').then(res => res.text()); +const file2 = await fetch('/monarch/samplecode.tsx').then(res => res.text()); + +const editorContainer1 =
as HTMLDivElement; +const editorContainer2 =
as HTMLDivElement; + +root.append(editorContainer1); +root.append(editorContainer2); + +const editor1 = monaco.editor.create(editorContainer1, { + lineNumbers: 'off', + fontSize: 12, + lineDecorationsWidth: 0, + minimap: { enabled: false }, + guides: { indentation: false }, + folding: false, + theme: "vsc2", + value: file1, + language: 'javascript', + scrollBeyondLastLine: false, + renderLineHighlightOnlyWhenFocus: true, + tabSize: 2, +}); + +const editor2 = monaco.editor.create(editorContainer2, { + lineNumbers: 'off', + fontSize: 12, + lineDecorationsWidth: 0, + minimap: { enabled: false }, + guides: { indentation: false }, + folding: false, + theme: "vsc2", + value: file2, + language: 'typescript', + scrollBeyondLastLine: false, + renderLineHighlightOnlyWhenFocus: true, + tabSize: 2, +}); + +editor1.layout({ width: 700, height: 900 }); +editor2.layout({ width: 700, height: 900 }); + +updateTokenProvider(); +editor1.onDidChangeModelContent(updateTokenProvider); + +async function updateTokenProvider() { + const code = editor1.getModel()!.getValue(); + const blob = new Blob([code], { type: 'text/javascript' }); + const url = URL.createObjectURL(blob); + const mod = await import(url); + URL.revokeObjectURL(url); + monaco.languages.setMonarchTokensProvider('typescript', mod.tokenProvider); +} diff --git a/site/monarch/samplecode.tsx b/site/monarch/samplecode.tsx new file mode 100644 index 0000000..2f00960 --- /dev/null +++ b/site/monarch/samplecode.tsx @@ -0,0 +1,229 @@ +// sample 0 + +document.querySelectorAll('.sample'); +function foo$>(): Required> { + return null as any; +} + +function foo$two(a: any) { + return undefined as T; +} + +foo$two(foo$()); + +// sample 1 + +export function ClickMe() { + let i = 0; + const el = as HTMLButtonElement; + el.onclick = (e) => { + el.textContent = `Clicked ${++i} times`; + }; + return el; +} + +// sample 2 + +export default () => <> +

+

+

+; + +// sample 3 + +function TodoInput(attrs: { add: (v: string) => void }) { + const input = as HTMLInputElement; + input.placeholder = 'Add todo item...'; + input.onkeydown = (e) => { + if (e.key === 'Enter') { + attrs.add(input.value); + input.value = ''; + } + }; + return input; +} + +class TodoList1 { + ul =
    as HTMLUListElement; + add(v: string) { + const item =
  • {v}
  • as HTMLLIElement; + item.onclick = () => item.remove(); + this.ul.append(item); + } +} + +export const sample3 = () => { + const list = new TodoList1(); + list.add('foo'); + list.add('bar'); + return <> + list.add(v)} /> + {list.ul} + ; +}; + +// sample 4 + +declare const data: Map; +// Names of US citizens born in 1882 from ssa.gov + +export function FindNames() { + const status =

    as HTMLParagraphElement; + const results =

      as HTMLUListElement; + const input = as HTMLInputElement; + + updateMatches(); + function updateMatches() { + const regex = new RegExp(`(${input.value})`, 'gi'); + const matched = ([...data.entries()] + .filter(([k]) => k.match(regex))); + + const matches = (matched + .slice(0, 25) + .map(match => )); + + results.replaceChildren(...matches); + status.textContent = `${matched.length} / ${data.size}`; + } + + return
      {input}{status}{results}
      ; +} + +function Item1(attrs: { match: [string, number], regex: RegExp }) { + const [name, count] = attrs.match; + const total = ({count}); + return
    • + {total} +
    • ; +} + +function highlight(str: string, regex: RegExp) { + return str.replace(regex, '$1'); +} + +// sample 5 + +export const sample5 = () => <> + +; + +function TodoList() { + const list = new List(); + + list.add('foo'); + list.add('bar').toggle(); + list.add('qux'); + + const input = as HTMLInputElement; + input.onkeydown = (e) => { + if (e.key === 'Enter' && input.value.trim().length > 0) { + list.add(input.value); + input.value = ''; + } + }; + + return
      +
      {input}
      +
      + + + +
      + {list.ul} +
      ; +} + +class List extends EventTarget { + + ul =
        as HTMLUListElement; + items: Item[] = []; + itemUnlisteners = new Map void>(); + + add(text: string) { + const item = new Item(this, text); + this.items.push(item); + this.ul.append(item.li); + this.dispatchEvent(new Event('item-added')); + + this.itemUnlisteners.set(item, listen(item, 'toggled', () => { + this.dispatchEvent(new Event('item-toggled')); + })); + + return item; + } + + rem(item: Item) { + const unlisten = this.itemUnlisteners.get(item)!; + this.itemUnlisteners.delete(item); + unlisten(); + + this.items = this.items.filter(it => it !== item); + this.dispatchEvent(new Event('item-removed')); + } + + clearDone = () => this.doneItems().forEach(it => it.remove()); + invertAll = () => this.items.forEach(it => it.toggle()); + + doneItems = () => this.items.filter(it => it.done); + +} + +class Item extends EventTarget { + + done = false; + #checkbox = as HTMLInputElement; + li; + + constructor(private list: List, text: string) { + super(); + this.li = ( +
      • + {this.#checkbox} + this.toggle()}>{text} + +
      • as HTMLLIElement + ); + this.#checkbox.onclick = () => this.toggle(); + } + + remove() { + this.li.remove(); + this.list.rem(this); + } + + toggle() { + this.done = !this.done; + this.li.classList.toggle('done', this.done); + this.#checkbox.checked = this.done; + this.dispatchEvent(new Event('toggled')); + } + +} + +function Counter({ list }: { list: List }) { + const span = as HTMLSpanElement; + + const updateText = () => { + const done = list.doneItems().length; + const total = list.items.length; + span.textContent = `Done: ${done}/${total}`; + }; + + updateText(); + list.addEventListener('item-added', updateText); + list.addEventListener('item-removed', updateText); + list.addEventListener('item-toggled', updateText); + + return span; +} + +function listen(target: EventTarget, event: string, fn: () => void) { + target.addEventListener(event, fn); + return () => target.removeEventListener(event, fn); +} diff --git a/site/monarch/style.css b/site/monarch/style.css new file mode 100644 index 0000000..b62fc16 --- /dev/null +++ b/site/monarch/style.css @@ -0,0 +1,10 @@ +body { + background-color: #22272e; + color: #c5d1de; + font-family: system-ui; + + #root { + display: flex; + gap: 1em; + } +} \ No newline at end of file diff --git a/site/theme.ts b/site/theme.ts new file mode 100644 index 0000000..2347be4 --- /dev/null +++ b/site/theme.ts @@ -0,0 +1,18 @@ +import monaco from '@imlib/monaco-esm'; + +export function setupTheme() { + monaco.editor.defineTheme('vsc2', { + base: 'vs-dark', + inherit: true, + rules: [ + // { token: "identifier.ts", foreground: "9CDCFE" }, + { token: "property.ts", foreground: "9CDCFE" }, + { token: "function.ts", foreground: "DCDCAA" }, + { token: "method.ts", foreground: "DCDCAA" }, + // { token: "delimiter.ts", foreground: "569CD6" }, + ], + colors: { + "editor.background": '#1b1f25', + }, + }); +} diff --git a/site/token-provider.tsx b/site/token-provider.ts similarity index 51% rename from site/token-provider.tsx rename to site/token-provider.ts index b9c6f0c..ac4abf8 100644 --- a/site/token-provider.tsx +++ b/site/token-provider.ts @@ -1,24 +1,38 @@ -import monaco from 'monaco-editor/esm/vs/editor/editor.api.js'; +// Based on https://github.com/microsoft/monaco-editor/blob/main/src/basic-languages/typescript/typescript.ts +// vs-dark monaco: https://github.com/microsoft/vscode/blob/main/src/vs/editor/standalone/common/themes.ts#L82 +// dark-plus vscode: https://github.com/microsoft/vscode/blob/main/extensions/theme-defaults/themes/dark_plus.json -// Borrowed from https://github.com/microsoft/monaco-editor/blob/main/src/basic-languages/typescript/typescript.ts -// (I'll give it back when I'm done) +export const tokenProvider = { -export const tokenProvider: monaco.languages.IMonarchLanguage = { defaultToken: 'invalid', tokenPostfix: '.ts', keywords: [ - 'abstract', 'any', 'as', 'asserts', 'bigint', 'boolean', 'break', 'case', 'catch', 'class', 'continue', 'const', 'constructor', 'debugger', 'declare', 'default', - 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'from', 'function', 'get', 'if', 'implements', 'import', 'in', 'infer', 'instanceof', - 'interface', 'is', 'keyof', 'let', 'module', 'namespace', 'never', 'new', 'null', 'number', 'object', 'out', 'package', 'private', 'protected', 'public', 'override', - 'readonly', 'require', 'global', 'return', 'satisfies', 'set', 'static', 'string', 'super', 'switch', 'symbol', 'this', 'throw', 'true', 'try', 'type', 'typeof', - 'undefined', 'unique', 'unknown', 'var', 'void', 'while', 'with', 'yield', 'async', 'await', 'of'], + // Should match the keys of textToKeywordObj in + // https://github.com/microsoft/TypeScript/blob/master/src/compiler/scanner.ts + 'abstract', 'any', 'as', 'asserts', 'bigint', 'boolean', 'break', + 'case', 'catch', 'class', 'continue', 'const', 'constructor', 'debugger', + 'declare', 'default', 'delete', 'do', 'else', 'enum', 'export', + 'extends', 'false', 'finally', 'for', 'from', 'function', 'get', + 'if', 'implements', 'import', 'in', 'infer', 'instanceof', 'interface', + 'is', 'keyof', 'let', 'module', 'namespace', 'never', 'new', + 'null', 'number', 'object', 'out', 'package', 'private', 'protected', + 'public', 'override', 'readonly', 'require', 'global', 'return', 'satisfies', + 'set', 'static', 'string', 'super', 'switch', 'symbol', 'this', + 'throw', 'true', 'try', 'type', 'typeof', 'undefined', 'unique', + 'unknown', 'var', 'void', 'while', 'with', 'yield', 'async', 'await', 'of' + ], operators: [ - '<=', '>=', '==', '!=', '===', '!==', '=>', '+', '-', '**', '*', '/', '%', '++', '--', '<<', '>', '>>>', '&', '|', '^', '!', '~', '&&', '||', - '??', '?', ':', '=', '+=', '-=', '*=', '**=', '/=', '%=', '<<=', '>>=', '>>>=', '&=', '|=', '^=', '@' + '<=', '>=', '==', '!=', '===', '!==', '=>', + '+', '-', '**', '*', '/', '%', '++', + '--', '<<', '>', '>>>', '&', '|', + '^', '!', '~', '&&', '||', '??', '?', + ':', '=', '+=', '-=', '*=', '**=', '/=', + '%=', '<<=', '>>=', '>>>=', '&=', '|=', '^=', '@' ], + // we include these common regular expressions symbols: /[=>](?!@symbols)/, '@brackets'], [/!(?=([^=]|$))/, 'delimiter'], - [/@symbols/, { cases: { '@operators': 'delimiter', '@default': '' } }], + [ + /@symbols/, + { + cases: { + '@operators': 'delimiter', + '@default': '' + } + } + ], // numbers [/(@digits)[eE]([\-+]?(@digits))?/, 'number.float'], @@ -86,39 +100,45 @@ export const tokenProvider: monaco.languages.IMonarchLanguage = { [/'([^'\\]|\\.)*$/, 'string.invalid'], // non-teminated string [/"/, 'string', '@string_double'], [/'/, 'string', '@string_single'], - [/`/, 'string', '@string_backtick'], + [/`/, 'string', '@string_backtick'] ], whitespace: [ [/[ \t\r\n]+/, ''], [/\/\*\*(?!\/)/, 'comment.doc', '@jsdoc'], [/\/\*/, 'comment', '@comment'], - [/\/\/.*$/, 'comment'], + [/\/\/.*$/, 'comment'] ], comment: [ [/[^\/*]+/, 'comment'], [/\*\//, 'comment', '@pop'], - [/[\/*]/, 'comment'], + [/[\/*]/, 'comment'] ], jsdoc: [ [/[^\/*]+/, 'comment.doc'], [/\*\//, 'comment.doc', '@pop'], - [/[\/*]/, 'comment.doc'], + [/[\/*]/, 'comment.doc'] ], // We match regular expression quite precisely regexp: [ - [/(\{)(\d+(?:,\d*)?)(\})/, ['regexp.escape.control', 'regexp.escape.control', 'regexp.escape.control']], - [/(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/, ['regexp.escape.control', { token: 'regexp.escape.control', next: '@regexrange' }]], + [ + /(\{)(\d+(?:,\d*)?)(\})/, + ['regexp.escape.control', 'regexp.escape.control', 'regexp.escape.control'] + ], + [ + /(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/, + ['regexp.escape.control', { token: 'regexp.escape.control', next: '@regexrange' }] + ], [/(\()(\?:|\?=|\?!)/, ['regexp.escape.control', 'regexp.escape.control']], [/[()]/, 'regexp.escape.control'], [/@regexpctl/, 'regexp.escape.control'], [/[^\\\/]/, 'regexp'], [/@regexpesc/, 'regexp.escape'], [/\\\./, 'regexp.invalid'], - [/(\/)([dgimsuy]*)/, [{ token: 'regexp', bracket: '@close', next: '@pop' }, 'keyword.other']], + [/(\/)([dgimsuy]*)/, [{ token: 'regexp', bracket: '@close', next: '@pop' }, 'keyword.other']] ], regexrange: [ @@ -126,21 +146,28 @@ export const tokenProvider: monaco.languages.IMonarchLanguage = { [/\^/, 'regexp.invalid'], [/@regexpesc/, 'regexp.escape'], [/[^\]]/, 'regexp'], - [/\]/, { token: 'regexp.escape.control', next: '@pop', bracket: '@close' }], + [ + /\]/, + { + token: 'regexp.escape.control', + next: '@pop', + bracket: '@close' + } + ] ], string_double: [ [/[^\\"]+/, 'string'], [/@escapes/, 'string.escape'], [/\\./, 'string.escape.invalid'], - [/"/, 'string', '@pop'], + [/"/, 'string', '@pop'] ], string_single: [ [/[^\\']+/, 'string'], [/@escapes/, 'string.escape'], [/\\./, 'string.escape.invalid'], - [/'/, 'string', '@pop'], + [/'/, 'string', '@pop'] ], string_backtick: [ @@ -148,18 +175,14 @@ export const tokenProvider: monaco.languages.IMonarchLanguage = { [/[^\\`$]+/, 'string'], [/@escapes/, 'string.escape'], [/\\./, 'string.escape.invalid'], - [/`/, 'string', '@pop'], - ], - - typeparams: [ - [/>/, 'delimiter', '@pop'], - { include: 'common' }, + [/`/, 'string', '@pop'] ], bracketCounting: [ [/\{/, 'delimiter.bracket', '@bracketCounting'], [/\}/, 'delimiter.bracket', '@pop'], - { include: 'common' }, + { include: 'common' } ] } + };