diff options
author | João Moreno <joao.moreno@microsoft.com> | 2022-07-19 15:25:52 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-07-19 15:25:52 +0300 |
commit | dc755925909f87f6fc44c2d29f3334192eba505c (patch) | |
tree | f1f67973a9a395f9b1687fa8b024df361fe4dac8 /src | |
parent | 4b95a2d3edef09299f0f1626b08d8049a91f85dd (diff) |
fix type issues in h() (#155600)
- improve regex with named capture groups
- drop $ in favor of inline id
- add tests
Co-authored-by: Henning Dieterichs <notify.henning.dieterichs@live.de>
Co-authored-by: Henning Dieterichs <notify.henning.dieterichs@live.de>
Diffstat (limited to 'src')
-rw-r--r-- | src/vs/base/browser/dom.ts | 113 | ||||
-rw-r--r-- | src/vs/base/browser/ui/tree/abstractTree.ts | 8 | ||||
-rw-r--r-- | src/vs/base/test/browser/dom.test.ts | 157 | ||||
-rw-r--r-- | src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts | 12 |
4 files changed, 231 insertions, 59 deletions
diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 4db47cd947a..4019ea90b61 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1738,56 +1738,81 @@ type HTMLElementAttributeKeys<T> = Partial<{ [K in keyof T]: T[K] extends Functi type ElementAttributes<T> = HTMLElementAttributeKeys<T> & Record<string, any>; type RemoveHTMLElement<T> = T extends HTMLElement ? never : T; type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; -type ArrayToObj<T extends any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>; - -type TagToElement<T> = T extends `.${string}` - ? HTMLDivElement - : T extends `#${string}` - ? HTMLDivElement - : T extends `${infer TStart}#${string}` - ? TStart extends keyof HTMLElementTagNameMap - ? HTMLElementTagNameMap[TStart] +type ArrayToObj<T extends readonly any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>; +type HHTMLElementTagNameMap = HTMLElementTagNameMap & { '': HTMLDivElement }; + +type TagToElement<T> = T extends `${infer TStart}#${string}` + ? TStart extends keyof HHTMLElementTagNameMap + ? HHTMLElementTagNameMap[TStart] : HTMLElement : T extends `${infer TStart}.${string}` - ? TStart extends keyof HTMLElementTagNameMap - ? HTMLElementTagNameMap[TStart] + ? TStart extends keyof HHTMLElementTagNameMap + ? HHTMLElementTagNameMap[TStart] : HTMLElement : T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : HTMLElement; +type TagToElementAndId<TTag> = TTag extends `${infer TTag}@${infer TId}` + ? { element: TagToElement<TTag>; id: TId } + : { element: TagToElement<TTag>; id: 'root' }; + +type TagToRecord<TTag> = TagToElementAndId<TTag> extends { element: infer TElement; id: infer TId } + ? Record<(TId extends string ? TId : never) | 'root', TElement> + : never; + +type Child = HTMLElement | string | Record<string, HTMLElement>; +type Children = [] + | [Child] + | [Child, Child] + | [Child, Child, Child] + | [Child, Child, Child, Child] + | [Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]; + +const H_REGEX = /(?<tag>[\w\-]+)?(?:#(?<id>[\w\-]+))?(?<class>(?:\.(?:[\w\-]+))*)(?:@(?<name>(?:[\w\_])+))?/; + /** * A helper function to create nested dom nodes. * * * ```ts - * private readonly htmlElements = h('div.code-view', [ - * h('div.title', { $: 'title' }), + * const elements = h('div.code-view', [ + * h('div.title@title'), * h('div.container', [ - * h('div.gutter', { $: 'gutterDiv' }), - * h('div', { $: 'editor' }), + * h('div.gutter@gutterDiv'), + * h('div@editor'), * ]), * ]); - * private readonly editor = createEditor(this.htmlElements.editor); + * const editor = createEditor(elements.editor); * ``` */ -export function h<TTag extends string>( - tag: TTag -): (Record<'root', TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; -export function h<TTag extends string, TId extends string>( - tag: TTag, - attributes: { $: TId } & Partial<ElementAttributes<TagToElement<TTag>>> -): Record<TId | 'root', TagToElement<TTag>>; -export function h<TTag extends string, T extends (HTMLElement | string | Record<string, HTMLElement>)[]>( - tag: TTag, - children: T -): (ArrayToObj<T> & Record<'root', TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; -export function h<TTag extends string>(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>): Record<'root', TagToElement<TTag>>; -export function h<TTag extends string, TId extends string, T extends (HTMLElement | string | Record<string, HTMLElement>)[]>( - tag: TTag, - attributes: { $: TId } & Partial<ElementAttributes<TagToElement<TTag>>>, - children: T -): (ArrayToObj<T> & Record<TId, TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; +export function h<TTag extends string> + (tag: TTag): + TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function h<TTag extends string, T extends Children> + (tag: TTag, children: T): + (ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function h<TTag extends string> + (tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>): + TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function h<TTag extends string, T extends Children> + (tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>, children: T): + (ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + export function h(tag: string, ...args: [] | [attributes: { $: string } & Partial<ElementAttributes<HTMLElement>> | Record<string, any>, children?: any[]] | [children: any[]]): Record<string, HTMLElement> { let attributes: { $?: string } & Partial<ElementAttributes<HTMLElement>>; let children: (Record<string, HTMLElement> | HTMLElement)[] | undefined; @@ -1800,25 +1825,29 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia children = args[1]; } - const match = SELECTOR_REGEX.exec(tag); + const match = H_REGEX.exec(tag); - if (!match) { + if (!match || !match.groups) { throw new Error('Bad use of h'); } - const tagName = match[1] || 'div'; + const tagName = match.groups['tag'] || 'div'; const el = document.createElement(tagName); - if (match[3]) { - el.id = match[3]; + if (match.groups['id']) { + el.id = match.groups['id']; } - if (match[4]) { - el.className = match[4].replace(/\./g, ' ').trim(); + if (match.groups['class']) { + el.className = match.groups['class'].replace(/\./g, ' ').trim(); } const result: Record<string, HTMLElement> = {}; + if (match.groups['name']) { + result[match.groups['name']] = el; + } + if (children) { for (const c of children) { if (c instanceof HTMLElement) { @@ -1833,10 +1862,6 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia } for (const [key, value] of Object.entries(attributes)) { - if (key === '$') { - result[value] = el; - continue; - } if (key === 'style') { for (const [cssKey, cssValue] of Object.entries(value)) { el.style.setProperty( diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index ba4c6ec3af1..95032ae5ee4 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -684,10 +684,10 @@ export enum TreeFindMode { class FindWidget<T, TFilterData> extends Disposable { - private readonly elements = h('div.monaco-tree-type-filter', [ - h('div.monaco-tree-type-filter-grab.codicon.codicon-debug-gripper', { $: 'grab' }), - h('div.monaco-tree-type-filter-input', { $: 'findInput' }), - h('div.monaco-tree-type-filter-actionbar', { $: 'actionbar' }), + private readonly elements = h('.monaco-tree-type-filter', [ + h('.monaco-tree-type-filter-grab.codicon.codicon-debug-gripper@grab'), + h('.monaco-tree-type-filter-input@findInput'), + h('.monaco-tree-type-filter-actionbar@actionbar'), ]); set mode(mode: TreeFindMode) { diff --git a/src/vs/base/test/browser/dom.test.ts b/src/vs/base/test/browser/dom.test.ts index 435f0066b9c..1ad53561927 100644 --- a/src/vs/base/test/browser/dom.test.ts +++ b/src/vs/base/test/browser/dom.test.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as dom from 'vs/base/browser/dom'; -const $ = dom.$; +import { $, h, multibyteAwareBtoa } from 'vs/base/browser/dom'; suite('dom', () => { test('hasClass', () => { @@ -73,9 +72,9 @@ suite('dom', () => { }); test('multibyteAwareBtoa', () => { - assert.ok(dom.multibyteAwareBtoa('hello world').length > 0); - assert.ok(dom.multibyteAwareBtoa('平仮名').length > 0); - assert.ok(dom.multibyteAwareBtoa(new Array(100000).fill('vs').join('')).length > 0); // https://github.com/microsoft/vscode/issues/112013 + assert.ok(multibyteAwareBtoa('hello world').length > 0); + assert.ok(multibyteAwareBtoa('平仮名').length > 0); + assert.ok(multibyteAwareBtoa(new Array(100000).fill('vs').join('')).length > 0); // https://github.com/microsoft/vscode/issues/112013 }); suite('$', () => { @@ -129,4 +128,152 @@ suite('dom', () => { assert.strictEqual(firstChild.textContent, 'foobar'); }); }); + + suite('h', () => { + test('should build simple nodes', () => { + const div = h('div'); + assert(div.root instanceof HTMLElement); + assert.strictEqual(div.root.tagName, 'DIV'); + + const span = h('span'); + assert(span.root instanceof HTMLElement); + assert.strictEqual(span.root.tagName, 'SPAN'); + + const img = h('img'); + assert(img.root instanceof HTMLElement); + assert.strictEqual(img.root.tagName, 'IMG'); + }); + + test('should handle ids and classes', () => { + const divId = h('div#myid'); + assert.strictEqual(divId.root.tagName, 'DIV'); + assert.strictEqual(divId.root.id, 'myid'); + + const divClass = h('div.a'); + assert.strictEqual(divClass.root.tagName, 'DIV'); + assert.strictEqual(divClass.root.classList.length, 1); + assert(divClass.root.classList.contains('a')); + + const divClasses = h('div.a.b.c'); + assert.strictEqual(divClasses.root.tagName, 'DIV'); + assert.strictEqual(divClasses.root.classList.length, 3); + assert(divClasses.root.classList.contains('a')); + assert(divClasses.root.classList.contains('b')); + assert(divClasses.root.classList.contains('c')); + + const divAll = h('div#myid.a.b.c'); + assert.strictEqual(divAll.root.tagName, 'DIV'); + assert.strictEqual(divAll.root.id, 'myid'); + assert.strictEqual(divAll.root.classList.length, 3); + assert(divAll.root.classList.contains('a')); + assert(divAll.root.classList.contains('b')); + assert(divAll.root.classList.contains('c')); + + const spanId = h('span#myid'); + assert.strictEqual(spanId.root.tagName, 'SPAN'); + assert.strictEqual(spanId.root.id, 'myid'); + + const spanClass = h('span.a'); + assert.strictEqual(spanClass.root.tagName, 'SPAN'); + assert.strictEqual(spanClass.root.classList.length, 1); + assert(spanClass.root.classList.contains('a')); + + const spanClasses = h('span.a.b.c'); + assert.strictEqual(spanClasses.root.tagName, 'SPAN'); + assert.strictEqual(spanClasses.root.classList.length, 3); + assert(spanClasses.root.classList.contains('a')); + assert(spanClasses.root.classList.contains('b')); + assert(spanClasses.root.classList.contains('c')); + + const spanAll = h('span#myid.a.b.c'); + assert.strictEqual(spanAll.root.tagName, 'SPAN'); + assert.strictEqual(spanAll.root.id, 'myid'); + assert.strictEqual(spanAll.root.classList.length, 3); + assert(spanAll.root.classList.contains('a')); + assert(spanAll.root.classList.contains('b')); + assert(spanAll.root.classList.contains('c')); + }); + + test('should implicitly handle ids and classes', () => { + const divId = h('#myid'); + assert.strictEqual(divId.root.tagName, 'DIV'); + assert.strictEqual(divId.root.id, 'myid'); + + const divClass = h('.a'); + assert.strictEqual(divClass.root.tagName, 'DIV'); + assert.strictEqual(divClass.root.classList.length, 1); + assert(divClass.root.classList.contains('a')); + + const divClasses = h('.a.b.c'); + assert.strictEqual(divClasses.root.tagName, 'DIV'); + assert.strictEqual(divClasses.root.classList.length, 3); + assert(divClasses.root.classList.contains('a')); + assert(divClasses.root.classList.contains('b')); + assert(divClasses.root.classList.contains('c')); + + const divAll = h('#myid.a.b.c'); + assert.strictEqual(divAll.root.tagName, 'DIV'); + assert.strictEqual(divAll.root.id, 'myid'); + assert.strictEqual(divAll.root.classList.length, 3); + assert(divAll.root.classList.contains('a')); + assert(divAll.root.classList.contains('b')); + assert(divAll.root.classList.contains('c')); + }); + + test('should handle @ identifiers', () => { + const implicit = h('@el'); + assert.strictEqual(implicit.root, implicit.el); + assert.strictEqual(implicit.el.tagName, 'DIV'); + + const explicit = h('div@el'); + assert.strictEqual(explicit.root, explicit.el); + assert.strictEqual(explicit.el.tagName, 'DIV'); + + const implicitId = h('#myid@el'); + assert.strictEqual(implicitId.root, implicitId.el); + assert.strictEqual(implicitId.el.tagName, 'DIV'); + assert.strictEqual(implicitId.root.id, 'myid'); + + const explicitId = h('div#myid@el'); + assert.strictEqual(explicitId.root, explicitId.el); + assert.strictEqual(explicitId.el.tagName, 'DIV'); + assert.strictEqual(explicitId.root.id, 'myid'); + + const implicitClass = h('.a@el'); + assert.strictEqual(implicitClass.root, implicitClass.el); + assert.strictEqual(implicitClass.el.tagName, 'DIV'); + assert.strictEqual(implicitClass.root.classList.length, 1); + assert(implicitClass.root.classList.contains('a')); + + const explicitClass = h('div.a@el'); + assert.strictEqual(explicitClass.root, explicitClass.el); + assert.strictEqual(explicitClass.el.tagName, 'DIV'); + assert.strictEqual(explicitClass.root.classList.length, 1); + assert(explicitClass.root.classList.contains('a')); + }); + }); + + test('should recurse', () => { + const result = h('div.code-view', [ + h('div.title@title'), + h('div.container', [ + h('div.gutter@gutterDiv'), + h('span@editor'), + ]), + ]); + + assert.strictEqual(result.root.tagName, 'DIV'); + assert.strictEqual(result.root.className, 'code-view'); + assert.strictEqual(result.root.childElementCount, 2); + assert.strictEqual(result.root.firstElementChild, result.title); + assert.strictEqual(result.title.tagName, 'DIV'); + assert.strictEqual(result.title.className, 'title'); + assert.strictEqual(result.title.childElementCount, 0); + assert.strictEqual(result.gutterDiv.tagName, 'DIV'); + assert.strictEqual(result.gutterDiv.className, 'gutter'); + assert.strictEqual(result.gutterDiv.childElementCount, 0); + assert.strictEqual(result.editor.tagName, 'SPAN'); + assert.strictEqual(result.editor.className, ''); + assert.strictEqual(result.editor.childElementCount, 0); + }); }); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts index 321d1594dd5..d1988a2cfd9 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts @@ -24,14 +24,14 @@ export abstract class CodeEditorView extends Disposable { readonly model = this._viewModel.map(m => /** @description model */ m?.model); protected readonly htmlElements = h('div.code-view', [ - h('div.title', { $: 'header' }, [ - h('span.title', { $: 'title' }), - h('span.description', { $: 'description' }), - h('span.detail', { $: 'detail' }), + h('div.title@header', [ + h('span.title@title'), + h('span.description@description'), + h('span.detail@detail'), ]), h('div.container', [ - h('div.gutter', { $: 'gutterDiv' }), - h('div', { $: 'editor' }), + h('div.gutter@gutterDiv'), + h('div@editor'), ]), ]); |