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>
This commit is contained in:
João Moreno 2022-07-19 14:25:52 +02:00 committed by GitHub
parent 4b95a2d3ed
commit dc75592590
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 230 additions and 58 deletions

View file

@ -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 ArrayToObj<T extends readonly any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>;
type HHTMLElementTagNameMap = HTMLElementTagNameMap & { '': HTMLDivElement };
type TagToElement<T> = T extends `.${string}`
? HTMLDivElement
: T extends `#${string}`
? HTMLDivElement
: T extends `${infer TStart}#${string}`
? TStart extends keyof HTMLElementTagNameMap
? HTMLElementTagNameMap[TStart]
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(

View file

@ -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) {

View file

@ -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);
});
});

View file

@ -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'),
]),
]);