From b85ed5ee235661c0a2107b86c06b6f3b9541545d Mon Sep 17 00:00:00 2001 From: jiko21 Date: Sun, 23 Nov 2025 06:52:06 +0900 Subject: [PATCH 1/8] add: html tag --- src/html.ts | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/src/html.ts b/src/html.ts index b585018..7e0d4e3 100644 --- a/src/html.ts +++ b/src/html.ts @@ -93,6 +93,161 @@ export type P = T extends Div = { + __error: `❌

cannot contain block elements. Only inline elements are allowed in

.`; + __invalidType: T; +}; + +declare const h1Brand: unique symbol; + +export type H1 = T extends Div + ? InvalidH1Content + : T extends Html + ? InvalidH1Content + : T extends Body + ? InvalidH1Content + : T extends P + ? InvalidH1Content + : T extends HTMLElement[] + ? { + [h1Brand]: 'h1'; + children: T; + } + : T extends HTMLElement + ? { + [h1Brand]: 'h1'; + children: T; + } + : never; + +type InvalidH2Content = { + __error: `❌

cannot contain block elements. Only inline elements are allowed in

.`; + __invalidType: T; +}; + +declare const h2Brand: unique symbol; + +export type H2 = T extends Div + ? InvalidH2Content + : T extends Html + ? InvalidH2Content + : T extends Body + ? InvalidH2Content + : T extends P + ? InvalidH2Content + : T extends H1 + ? InvalidH2Content + : T extends HTMLElement[] + ? { + [h2Brand]: 'h2'; + children: T; + } + : T extends HTMLElement + ? { + [h2Brand]: 'h2'; + children: T; + } + : never; + +type InvalidH3Content = { + __error: `❌

cannot contain block elements. Only inline elements are allowed in

.`; + __invalidType: T; +}; + +declare const h3Brand: unique symbol; + +export type H3 = T extends Div + ? InvalidH3Content + : T extends Html + ? InvalidH3Content + : T extends Body + ? InvalidH3Content + : T extends P + ? InvalidH3Content + : T extends H1 + ? InvalidH3Content + : T extends H2 + ? InvalidH3Content + : T extends HTMLElement[] + ? { + [h3Brand]: 'h3'; + children: T; + } + : T extends HTMLElement + ? { + [h3Brand]: 'h3'; + children: T; + } + : never; + +type InvalidH4Content = { + __error: `❌

cannot contain block elements. Only inline elements are allowed in

.`; + __invalidType: T; +}; + +declare const h4Brand: unique symbol; + +export type H4 = T extends Div + ? InvalidH4Content + : T extends Html + ? InvalidH4Content + : T extends Body + ? InvalidH4Content + : T extends P + ? InvalidH4Content + : T extends H1 + ? InvalidH4Content + : T extends H2 + ? InvalidH4Content + : T extends H3 + ? InvalidH4Content + : T extends HTMLElement[] + ? { + [h4Brand]: 'h4'; + children: T; + } + : T extends HTMLElement + ? { + [h4Brand]: 'h4'; + children: T; + } + : never; + +type InvalidH5Content = { + __error: `❌
cannot contain block elements. Only inline elements are allowed in
.`; + __invalidType: T; +}; + +declare const h5Brand: unique symbol; + +export type H5 = T extends Div + ? InvalidH5Content + : T extends Html + ? InvalidH5Content + : T extends Body + ? InvalidH5Content + : T extends P + ? InvalidH5Content + : T extends H1 + ? InvalidH5Content + : T extends H2 + ? InvalidH5Content + : T extends H3 + ? InvalidH5Content + : T extends H4 + ? InvalidH5Content + : T extends HTMLElement[] + ? { + [h5Brand]: 'h5'; + children: T; + } + : T extends HTMLElement + ? { + [h5Brand]: 'h5'; + children: T; + } + : never; + // HTML JSON構造体 export type HtmlJson = { tag: string; From 168adaa9fc95c003ec52f920d76304d585fb185c Mon Sep 17 00:00:00 2001 From: jiko21 Date: Mon, 24 Nov 2025 20:50:40 +0900 Subject: [PATCH 2/8] update: html tag --- src/html.ts | 130 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 90 insertions(+), 40 deletions(-) diff --git a/src/html.ts b/src/html.ts index 7e0d4e3..7c04ea6 100644 --- a/src/html.ts +++ b/src/html.ts @@ -6,41 +6,51 @@ export type HTMLElement = | Text | { children: (HTMLElement | Text)[] | HTMLElement | Text; + attributes?: Record; }; // HTML Brand Types declare const htmlBrand: unique symbol; -export type Html = T extends HTMLElement[] +export type Html< + T extends HTMLElement[] | HTMLElement, + A extends Record = {} +> = T extends HTMLElement[] ? { [htmlBrand]: 'html'; children: T; + attributes: A; } : T extends HTMLElement ? { [htmlBrand]: 'html'; children: T; + attributes: A; } : T extends Text ? { [htmlBrand]: 'html'; children: T; + attributes: A; } : never; declare const bodyBrand: unique symbol; -export type Body = T extends Html< - HTMLElement | HTMLElement[] -> +export type Body< + T extends HTMLElement[] | HTMLElement, + A extends Record = {} +> = T extends Html ? never : T extends HTMLElement[] ? { [bodyBrand]: 'body'; children: T; + attributes: A; } : T extends HTMLElement ? { [bodyBrand]: 'body'; children: T; + attributes: A; } : never; @@ -52,19 +62,24 @@ type InvalidDivContent = { declare const divBrand: unique symbol; -export type Div = T extends Html +export type Div< + T extends HTMLElement[] | HTMLElement, + A extends Record = {} +> = T extends Html ? InvalidDivContent - : T extends Body + : T extends Body ? InvalidDivContent : T extends HTMLElement[] ? { [divBrand]: 'div'; children: T; + attributes: A; } : T extends HTMLElement ? { [divBrand]: 'div'; children: T; + attributes: A; } : never; @@ -75,21 +90,26 @@ type InvalidPContent = { declare const pBrand: unique symbol; -export type P = T extends Div +export type P< + T extends HTMLElement[] | HTMLElement, + A extends Record = {} +> = T extends Div ? InvalidPContent - : T extends Html + : T extends Html ? InvalidPContent - : T extends Body + : T extends Body ? InvalidPContent : T extends HTMLElement[] ? { [pBrand]: 'p'; children: T; + attributes: A; } : T extends HTMLElement ? { [pBrand]: 'p'; children: T; + attributes: A; } : never; @@ -100,23 +120,28 @@ type InvalidH1Content = { declare const h1Brand: unique symbol; -export type H1 = T extends Div +export type H1< + T extends HTMLElement[] | HTMLElement, + A extends Record = {} +> = T extends Div ? InvalidH1Content - : T extends Html + : T extends Html ? InvalidH1Content - : T extends Body + : T extends Body ? InvalidH1Content - : T extends P + : T extends P ? InvalidH1Content : T extends HTMLElement[] ? { [h1Brand]: 'h1'; children: T; + attributes: A; } : T extends HTMLElement ? { [h1Brand]: 'h1'; children: T; + attributes: A; } : never; @@ -127,25 +152,30 @@ type InvalidH2Content = { declare const h2Brand: unique symbol; -export type H2 = T extends Div +export type H2< + T extends HTMLElement[] | HTMLElement, + A extends Record = {} +> = T extends Div ? InvalidH2Content - : T extends Html + : T extends Html ? InvalidH2Content - : T extends Body + : T extends Body ? InvalidH2Content - : T extends P + : T extends P ? InvalidH2Content - : T extends H1 + : T extends H1 ? InvalidH2Content : T extends HTMLElement[] ? { [h2Brand]: 'h2'; children: T; + attributes: A; } : T extends HTMLElement ? { [h2Brand]: 'h2'; children: T; + attributes: A; } : never; @@ -156,27 +186,32 @@ type InvalidH3Content = { declare const h3Brand: unique symbol; -export type H3 = T extends Div +export type H3< + T extends HTMLElement[] | HTMLElement, + A extends Record = {} +> = T extends Div ? InvalidH3Content - : T extends Html + : T extends Html ? InvalidH3Content - : T extends Body + : T extends Body ? InvalidH3Content - : T extends P + : T extends P ? InvalidH3Content - : T extends H1 + : T extends H1 ? InvalidH3Content - : T extends H2 + : T extends H2 ? InvalidH3Content : T extends HTMLElement[] ? { [h3Brand]: 'h3'; children: T; + attributes: A; } : T extends HTMLElement ? { [h3Brand]: 'h3'; children: T; + attributes: A; } : never; @@ -187,29 +222,34 @@ type InvalidH4Content = { declare const h4Brand: unique symbol; -export type H4 = T extends Div +export type H4< + T extends HTMLElement[] | HTMLElement, + A extends Record = {} +> = T extends Div ? InvalidH4Content - : T extends Html + : T extends Html ? InvalidH4Content - : T extends Body + : T extends Body ? InvalidH4Content - : T extends P + : T extends P ? InvalidH4Content - : T extends H1 + : T extends H1 ? InvalidH4Content - : T extends H2 + : T extends H2 ? InvalidH4Content - : T extends H3 + : T extends H3 ? InvalidH4Content : T extends HTMLElement[] ? { [h4Brand]: 'h4'; children: T; + attributes: A; } : T extends HTMLElement ? { [h4Brand]: 'h4'; children: T; + attributes: A; } : never; @@ -220,31 +260,36 @@ type InvalidH5Content = { declare const h5Brand: unique symbol; -export type H5 = T extends Div +export type H5< + T extends HTMLElement[] | HTMLElement, + A extends Record = {} +> = T extends Div ? InvalidH5Content - : T extends Html + : T extends Html ? InvalidH5Content - : T extends Body + : T extends Body ? InvalidH5Content - : T extends P + : T extends P ? InvalidH5Content - : T extends H1 + : T extends H1 ? InvalidH5Content - : T extends H2 + : T extends H2 ? InvalidH5Content - : T extends H3 + : T extends H3 ? InvalidH5Content - : T extends H4 + : T extends H4 ? InvalidH5Content : T extends HTMLElement[] ? { [h5Brand]: 'h5'; children: T; + attributes: A; } : T extends HTMLElement ? { [h5Brand]: 'h5'; children: T; + attributes: A; } : never; @@ -252,6 +297,10 @@ export type H5 = T extends Div\n`); + const attributes = input.attributes?.map((item) => `${item.key}="${item.value}"`).join(" "); + writeStream.write(`${space}<${input.tag}${attributes ? ` ${attributes}` : ''}>\n`); input.children.forEach((item) => { if (typeof item === 'string') { writeStream.write(`${space} ${item}\n`); From 209ffe42175975df5c5a6b546f333aefbd0bd80f Mon Sep 17 00:00:00 2001 From: jiko21 Date: Mon, 24 Nov 2025 21:10:00 +0900 Subject: [PATCH 3/8] update: attributes --- src/tsUtil.test.ts | 2 +- src/tsUtil.ts | 97 +++++++++++++++++++++++++++++++--------------- 2 files changed, 67 insertions(+), 32 deletions(-) diff --git a/src/tsUtil.test.ts b/src/tsUtil.test.ts index 9779f88..dc09705 100644 --- a/src/tsUtil.test.ts +++ b/src/tsUtil.test.ts @@ -126,7 +126,7 @@ describe('TypeScript Utility Functions', () => { findNonTypeLiteral(sourceFile); if (nonTypeLiteralNode) { - expect(() => traverseNode(nonTypeLiteralNode!)).toThrow('Unexpected type'); + expect(() => traverseNode(nonTypeLiteralNode!, 0)).toThrow('Unexpected type'); } }); }); diff --git a/src/tsUtil.ts b/src/tsUtil.ts index 09d05cd..7c59241 100644 --- a/src/tsUtil.ts +++ b/src/tsUtil.ts @@ -1,45 +1,80 @@ // TypeScript AST Processing Utilities -import { createWriteStream } from 'node:fs'; -import ts, { SyntaxKind } from 'typescript'; -import { type HtmlJson, renderToStream } from './html'; +import {createWriteStream} from 'node:fs'; +import ts from 'typescript'; +import {type HtmlJson, renderToStream} from './html'; +import assert from "node:assert"; + +function parseAttributes(node: ts.Node | undefined): HtmlJson['attributes'] | undefined { + if (node && ts.isTypeLiteralNode(node)) { + return node.members.map((obj) => { + if ( + ts.isPropertySignature(obj) && + ts.isIdentifier(obj.name) && + obj.type && + ts.isLiteralTypeNode(obj.type) && + ts.isStringLiteral(obj.type.literal) + ) { + return {key: obj.name.escapedText.toString(), value: obj.type.literal.text} + } else { + throw new Error('Invalid attribute was found in attributes.') + } + }); + } + throw new Error('Unexpected error when parsing attributes.'); +} + +function parseChild(node: ts.Node, indent: number): HtmlJson | string { + if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) { + return node.literal.text; + } else if (ts.isTypeLiteralNode(node)) { + return traverseNode(node, indent); + } else { + throw new Error('unexpected type while parsing children elements.') + } +} + +function parseChildren(node: ts.Node, indent: number): HtmlJson['children'] { + // node should be propertySignature + assert(ts.isPropertySignature(node)); + // type is not undefined + assert(node.type) + if (ts.isTypeLiteralNode(node.type)) { + return [traverseNode(node.type, indent + 2)]; + } else if (ts.isLiteralTypeNode(node.type) && ts.isStringLiteral(node.type.literal)) { + return [node.type.literal.text]; + } else if (ts.isTupleTypeNode(node.type)) { + return node.type.elements.map((val) => parseChild(val, indent)); + } else { + throw new Error("Unexpected error when parsing children."); + } +} export function traverseNode(node: ts.Node, indent: number = 0): HtmlJson { if (ts.isTypeLiteralNode(node)) { let tag = ''; let children: (HtmlJson | string)[] = []; - node.members.forEach((member) => { - if (member.name && ts.isPropertySignature(member) && ts.isIdentifier(member.name)) { - if ( - member.type && - ts.isLiteralTypeNode(member.type) && - ts.isStringLiteral(member.type.literal) - ) { - children = [member.type.literal.text]; - } else if (member.type && ts.isPropertySignature(member)) { - if (ts.isTupleTypeNode(member.type)) { - // loop - children = member.type.elements - .filter((type) => type) - .map((type) => traverseNode(type, indent + 2)); - } else if (ts.isTypeLiteralNode(member.type)) { - children = [traverseNode(member.type, indent + 2)]; - } + let attributes: { key: string, value: string }[] | undefined; + node.members.forEach(member => { + if (ts.isPropertySignature(member) && ts.isComputedPropertyName(member.name) && ts.isIdentifier(member.name.expression)) { + tag = member.name.expression.escapedText.toString().replace('Brand', ''); + } else if (ts.isPropertySignature(member) && ts.isIdentifier(member.name) && member.type) { + switch (member.name.escapedText) { + case 'attributes': + attributes = parseAttributes(member.type); + break; + case 'children': + children = parseChildren(member, indent + 2); + break; + default: + throw new Error(`unexpected type ${member.type}`); } - } else if ( - member.name && - ts.isComputedPropertyName(member.name) && - ts.isIdentifier(member.name.expression) && - member.name.expression.escapedText - ) { - tag = member.name.expression.escapedText.replace('Brand', ''); - } else { - throw new Error('Unexpected type'); } }); return { tag, children, + attributes, }; } else { throw new Error('Unexpected type'); @@ -63,7 +98,7 @@ export function visit(node: ts.Node, checker: ts.TypeChecker, outPath: string) { const typeNode = checker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.NoTruncation); if (typeNode) { const result = traverseNode(typeNode, 0); - const writeStream = createWriteStream(outPath, { flags: 'w' }); + const writeStream = createWriteStream(outPath, {flags: 'w'}); renderToStream(result, writeStream); writeStream.end('\n'); } @@ -106,7 +141,7 @@ export function processTypeScript(filePath: string, outPath: string): boolean { return false; } - const { sourceFile, checker } = result; + const {sourceFile, checker} = result; try { visit(sourceFile, checker, outPath); From 04c91f0edef175627a21c8cfb9edc59962096b9e Mon Sep 17 00:00:00 2001 From: jiko21 Date: Sat, 29 Nov 2025 12:24:17 +0900 Subject: [PATCH 4/8] update: add image --- src/html.ts | 12 ++++++++++++ src/tsUtil.ts | 28 +++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/html.ts b/src/html.ts index 7c04ea6..7bada43 100644 --- a/src/html.ts +++ b/src/html.ts @@ -293,6 +293,18 @@ export type H5< } : never; +declare const imageBrand: unique symbol; + +export type Image< + A extends Record = {} +> = { + [K in typeof imageBrand | 'children' | 'attributes']: + K extends typeof imageBrand ? 'image' + : K extends 'children' ? [] + : K extends 'attributes' ? A + : never; +} + // HTML JSON構造体 export type HtmlJson = { tag: string; diff --git a/src/tsUtil.ts b/src/tsUtil.ts index 7c59241..6cb141c 100644 --- a/src/tsUtil.ts +++ b/src/tsUtil.ts @@ -1,7 +1,7 @@ // TypeScript AST Processing Utilities import {createWriteStream} from 'node:fs'; -import ts from 'typescript'; +import ts, {SyntaxKind} from 'typescript'; import {type HtmlJson, renderToStream} from './html'; import assert from "node:assert"; @@ -29,8 +29,25 @@ function parseChild(node: ts.Node, indent: number): HtmlJson | string { return node.literal.text; } else if (ts.isTypeLiteralNode(node)) { return traverseNode(node, indent); + } else if (ts.isTypeReferenceNode(node)) { + // Handle Image and other void elements that may remain as TypeReference + if (ts.isIdentifier(node.typeName)) { + const tagName = node.typeName.escapedText.toString().toLowerCase(); + // Parse attributes from type arguments if present + let attributes: { key: string, value: string }[] | undefined; + if (node.typeArguments && node.typeArguments.length > 0) { + attributes = parseAttributes(node.typeArguments[0]); + } + return { + tag: tagName, + children: [], + attributes + }; + } + throw new Error(`unexpected type reference while parsing children elements.`) } else { - throw new Error('unexpected type while parsing children elements.') + console.log(node) + throw new Error(`unexpected type while parsing children elements. node kind is ${SyntaxKind[node.kind]}`) } } @@ -95,7 +112,12 @@ export function visit(node: ts.Node, checker: ts.TypeChecker, outPath: string) { ) { try { const type = checker.getTypeAtLocation(node); - const typeNode = checker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.NoTruncation); + const typeNode = checker.typeToTypeNode( + type, + undefined, + ts.NodeBuilderFlags.NoTruncation | + ts.NodeBuilderFlags.NoTypeReduction + ); if (typeNode) { const result = traverseNode(typeNode, 0); const writeStream = createWriteStream(outPath, {flags: 'w'}); From a2e324de8c86bb0e02108a375ed05be29913b890 Mon Sep 17 00:00:00 2001 From: jiko21 Date: Sat, 29 Nov 2025 13:10:39 +0900 Subject: [PATCH 5/8] update: add html dir --- src/html/attributes.ts | 135 ++++++++++++++++++++++++++++++++++ src/html/index.ts | 3 + src/{html.ts => html/tags.ts} | 80 +++++--------------- src/html/util.ts | 41 +++++++++++ 4 files changed, 196 insertions(+), 63 deletions(-) create mode 100644 src/html/attributes.ts create mode 100644 src/html/index.ts rename src/{html.ts => html/tags.ts} (79%) create mode 100644 src/html/util.ts diff --git a/src/html/attributes.ts b/src/html/attributes.ts new file mode 100644 index 0000000..51fc6bb --- /dev/null +++ b/src/html/attributes.ts @@ -0,0 +1,135 @@ +export interface GlobalAttributes { + id?: string; + class?: string; + style?: string; + title?: string; + lang?: string; + dir?: 'ltr' | 'rtl' | 'auto'; + tabindex?: number; + accesskey?: string; + contenteditable?: boolean | 'true' | 'false'; + draggable?: boolean | 'true' | 'false'; + hidden?: boolean; + spellcheck?: boolean | 'true' | 'false'; + translate?: 'yes' | 'no'; + [key: `data-${string}`]: string | number | boolean; + role?: string; + [key: `aria-${string}`]: string | number | boolean; +} + +export interface EventAttributes { + onclick?: string; + ondblclick?: string; + onmousedown?: string; + onmouseup?: string; + onmouseover?: string; + onmousemove?: string; + onmouseout?: string; + onkeydown?: string; + onkeyup?: string; + onkeypress?: string; + onfocus?: string; + onblur?: string; + onchange?: string; + onsubmit?: string; + onload?: string; + onunload?: string; + onresize?: string; + onscroll?: string; +} + +export interface FormAttributes { + name?: string; + value?: string | number; + type?: string; + placeholder?: string; + required?: boolean; + disabled?: boolean; + readonly?: boolean; + checked?: boolean; + selected?: boolean; + maxlength?: number; + minlength?: number; + max?: number | string; + min?: number | string; + step?: number | string; + pattern?: string; + autocomplete?: 'on' | 'off' | string; + autofocus?: boolean; + multiple?: boolean; + size?: number; + form?: string; + formaction?: string; + formenctype?: string; + formmethod?: 'get' | 'post'; + formnovalidate?: boolean; + formtarget?: '_blank' | '_self' | '_parent' | '_top' | string; +} + +export interface LinkResourceAttributes { + href?: string; + src?: string; + srcset?: string; + sizes?: string; + media?: string; + rel?: string; + type?: string; + download?: boolean | string; + target?: '_blank' | '_self' | '_parent' | '_top' | string; + hreflang?: string; + referrerpolicy?: 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'; +} + +export interface MediaAttributes { + alt?: string; + width?: number | string; + height?: number | string; + loading?: 'eager' | 'lazy'; + decoding?: 'sync' | 'async' | 'auto'; + crossorigin?: 'anonymous' | 'use-credentials'; + usemap?: string; + ismap?: boolean; + autoplay?: boolean; + controls?: boolean; + loop?: boolean; + muted?: boolean; + preload?: 'none' | 'metadata' | 'auto'; + poster?: string; +} + +export interface TableAttributes { + colspan?: number; + rowspan?: number; + headers?: string; + scope?: 'row' | 'col' | 'rowgroup' | 'colgroup'; +} + +export interface MetadataAttributes { + charset?: string; + content?: string; + 'http-equiv'?: string; + property?: string; +} + +export interface ListAttributes { + start?: number; + reversed?: boolean; + type?: '1' | 'a' | 'A' | 'i' | 'I'; +} + +export interface EmbeddedAttributes { + sandbox?: string; + allow?: string; + allowfullscreen?: boolean; + allowpaymentrequest?: boolean; +} + +export type AllHTMLAttributes = GlobalAttributes & + EventAttributes & + FormAttributes & + LinkResourceAttributes & + MediaAttributes & + TableAttributes & + MetadataAttributes & + ListAttributes & + EmbeddedAttributes; \ No newline at end of file diff --git a/src/html/index.ts b/src/html/index.ts new file mode 100644 index 0000000..2214dab --- /dev/null +++ b/src/html/index.ts @@ -0,0 +1,3 @@ +export * from './attributes'; +export * from './tags'; +export * from './util'; \ No newline at end of file diff --git a/src/html.ts b/src/html/tags.ts similarity index 79% rename from src/html.ts rename to src/html/tags.ts index 7bada43..3989c88 100644 --- a/src/html.ts +++ b/src/html/tags.ts @@ -1,4 +1,4 @@ -// HTML Type System - 型安全なHTML構造を定義 +import type { AllHTMLAttributes } from './attributes'; export type Text = string; @@ -6,14 +6,13 @@ export type HTMLElement = | Text | { children: (HTMLElement | Text)[] | HTMLElement | Text; - attributes?: Record; + attributes?: AllHTMLAttributes; }; -// HTML Brand Types declare const htmlBrand: unique symbol; export type Html< T extends HTMLElement[] | HTMLElement, - A extends Record = {} + A extends AllHTMLAttributes = {} > = T extends HTMLElement[] ? { [htmlBrand]: 'html'; @@ -37,7 +36,7 @@ export type Html< declare const bodyBrand: unique symbol; export type Body< T extends HTMLElement[] | HTMLElement, - A extends Record = {} + A extends AllHTMLAttributes = {} > = T extends Html ? never : T extends HTMLElement[] @@ -54,7 +53,6 @@ export type Body< } : never; -// エラー型定義 type InvalidDivContent = { __error: `❌
cannot contain or elements. Invalid HTML structure.`; __invalidType: T; @@ -64,7 +62,7 @@ declare const divBrand: unique symbol; export type Div< T extends HTMLElement[] | HTMLElement, - A extends Record = {} + A extends AllHTMLAttributes = {} > = T extends Html ? InvalidDivContent : T extends Body @@ -92,7 +90,7 @@ declare const pBrand: unique symbol; export type P< T extends HTMLElement[] | HTMLElement, - A extends Record = {} + A extends AllHTMLAttributes = {} > = T extends Div ? InvalidPContent : T extends Html @@ -122,7 +120,7 @@ declare const h1Brand: unique symbol; export type H1< T extends HTMLElement[] | HTMLElement, - A extends Record = {} + A extends AllHTMLAttributes = {} > = T extends Div ? InvalidH1Content : T extends Html @@ -154,7 +152,7 @@ declare const h2Brand: unique symbol; export type H2< T extends HTMLElement[] | HTMLElement, - A extends Record = {} + A extends AllHTMLAttributes = {} > = T extends Div ? InvalidH2Content : T extends Html @@ -188,7 +186,7 @@ declare const h3Brand: unique symbol; export type H3< T extends HTMLElement[] | HTMLElement, - A extends Record = {} + A extends AllHTMLAttributes = {} > = T extends Div ? InvalidH3Content : T extends Html @@ -224,7 +222,7 @@ declare const h4Brand: unique symbol; export type H4< T extends HTMLElement[] | HTMLElement, - A extends Record = {} + A extends AllHTMLAttributes = {} > = T extends Div ? InvalidH4Content : T extends Html @@ -262,7 +260,7 @@ declare const h5Brand: unique symbol; export type H5< T extends HTMLElement[] | HTMLElement, - A extends Record = {} + A extends AllHTMLAttributes = {} > = T extends Div ? InvalidH5Content : T extends Html @@ -293,58 +291,14 @@ export type H5< } : never; -declare const imageBrand: unique symbol; +declare const imgBrand: unique symbol; -export type Image< - A extends Record = {} +export type Img< + A extends AllHTMLAttributes = {} > = { - [K in typeof imageBrand | 'children' | 'attributes']: - K extends typeof imageBrand ? 'image' + [K in typeof imgBrand | 'children' | 'attributes']: + K extends typeof imgBrand ? 'img' : K extends 'children' ? [] : K extends 'attributes' ? A : never; -} - -// HTML JSON構造体 -export type HtmlJson = { - tag: string; - children: (HtmlJson | string)[]; - attributes: { - key: string; - value: string; - }[] | undefined; -}; - -// HTML レンダリング関数 -export function renderToHtml(input: HtmlJson, indent: number = 0): string { - const space = ' '.repeat(indent * 2); - return `${space}<${input.tag}> -${input.children - .map((item) => { - if (typeof item === 'string') { - return `${space} ${item}`; - } else { - return renderToHtml(item, indent + 1); - } - }) - .join('\n')} -${space}`; -} - -export function renderToStream( - input: HtmlJson, - writeStream: { write: (data: string) => void }, - indent: number = 0 -) { - const space = ' '.repeat(indent * 2); - const attributes = input.attributes?.map((item) => `${item.key}="${item.value}"`).join(" "); - writeStream.write(`${space}<${input.tag}${attributes ? ` ${attributes}` : ''}>\n`); - input.children.forEach((item) => { - if (typeof item === 'string') { - writeStream.write(`${space} ${item}\n`); - } else { - renderToStream(item, writeStream, indent + 1); - } - }); - writeStream.write(`${space}\n`); -} +} \ No newline at end of file diff --git a/src/html/util.ts b/src/html/util.ts new file mode 100644 index 0000000..f16b869 --- /dev/null +++ b/src/html/util.ts @@ -0,0 +1,41 @@ +export type HtmlJson = { + tag: string; + children: (HtmlJson | string)[]; + attributes: { + key: string; + value: string; + }[] | undefined; +}; + +export function renderToHtml(input: HtmlJson, indent: number = 0): string { + const space = ' '.repeat(indent * 2); + return `${space}<${input.tag}> +${input.children + .map((item) => { + if (typeof item === 'string') { + return `${space} ${item}`; + } else { + return renderToHtml(item, indent + 1); + } + }) + .join('\n')} +${space}`; +} + +export function renderToStream( + input: HtmlJson, + writeStream: { write: (data: string) => void }, + indent: number = 0 +) { + const space = ' '.repeat(indent * 2); + const attributes = input.attributes?.map((item) => `${item.key}="${item.value}"`).join(" "); + writeStream.write(`${space}<${input.tag}${attributes ? ` ${attributes}` : ''}>\n`); + input.children.forEach((item) => { + if (typeof item === 'string') { + writeStream.write(`${space} ${item}\n`); + } else { + renderToStream(item, writeStream, indent + 1); + } + }); + writeStream.write(`${space}\n`); +} \ No newline at end of file From 68ad0542bbc6caaea4b8917b49b6cd32bda6cd28 Mon Sep 17 00:00:00 2001 From: jiko21 Date: Sat, 29 Nov 2025 13:49:01 +0900 Subject: [PATCH 6/8] update --- src/html/tags.ts | 2 +- src/html/util.ts | 40 +++++++++++++++------------------------- src/tsUtil.ts | 4 ++-- 3 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/html/tags.ts b/src/html/tags.ts index 3989c88..d2e70f8 100644 --- a/src/html/tags.ts +++ b/src/html/tags.ts @@ -298,7 +298,7 @@ export type Img< > = { [K in typeof imgBrand | 'children' | 'attributes']: K extends typeof imgBrand ? 'img' - : K extends 'children' ? [] + : K extends 'children' ? never : K extends 'attributes' ? A : never; } \ No newline at end of file diff --git a/src/html/util.ts b/src/html/util.ts index f16b869..42cde6f 100644 --- a/src/html/util.ts +++ b/src/html/util.ts @@ -1,27 +1,13 @@ +const NON_CHILDREN_TAGS = ['img', 'link'] export type HtmlJson = { tag: string; - children: (HtmlJson | string)[]; + children: (HtmlJson | string)[] | undefined; attributes: { key: string; value: string; }[] | undefined; }; -export function renderToHtml(input: HtmlJson, indent: number = 0): string { - const space = ' '.repeat(indent * 2); - return `${space}<${input.tag}> -${input.children - .map((item) => { - if (typeof item === 'string') { - return `${space} ${item}`; - } else { - return renderToHtml(item, indent + 1); - } - }) - .join('\n')} -${space}`; -} - export function renderToStream( input: HtmlJson, writeStream: { write: (data: string) => void }, @@ -29,13 +15,17 @@ export function renderToStream( ) { const space = ' '.repeat(indent * 2); const attributes = input.attributes?.map((item) => `${item.key}="${item.value}"`).join(" "); - writeStream.write(`${space}<${input.tag}${attributes ? ` ${attributes}` : ''}>\n`); - input.children.forEach((item) => { - if (typeof item === 'string') { - writeStream.write(`${space} ${item}\n`); - } else { - renderToStream(item, writeStream, indent + 1); - } - }); - writeStream.write(`${space}\n`); + if (input.children) { + writeStream.write(`${space}<${input.tag}${attributes ? ` ${attributes}` : ''}>\n`); + input.children?.forEach((item) => { + if (typeof item === 'string') { + writeStream.write(`${space} ${item}\n`); + } else { + renderToStream(item, writeStream, indent + 1); + } + }); + writeStream.write(`${space}\n`); + } else { + writeStream.write(`${space}<${input.tag}${attributes ? ` ${attributes}` : ''} />\n`); + } } \ No newline at end of file diff --git a/src/tsUtil.ts b/src/tsUtil.ts index 6cb141c..8759417 100644 --- a/src/tsUtil.ts +++ b/src/tsUtil.ts @@ -40,7 +40,7 @@ function parseChild(node: ts.Node, indent: number): HtmlJson | string { } return { tag: tagName, - children: [], + children: undefined, attributes }; } @@ -70,7 +70,7 @@ function parseChildren(node: ts.Node, indent: number): HtmlJson['children'] { export function traverseNode(node: ts.Node, indent: number = 0): HtmlJson { if (ts.isTypeLiteralNode(node)) { let tag = ''; - let children: (HtmlJson | string)[] = []; + let children: (HtmlJson | string)[] | undefined = []; let attributes: { key: string, value: string }[] | undefined; node.members.forEach(member => { if (ts.isPropertySignature(member) && ts.isComputedPropertyName(member.name) && ts.isIdentifier(member.name.expression)) { From 8f576961e0735388b7c10ea19bfe3cded51c433c Mon Sep 17 00:00:00 2001 From: jiko21 Date: Sat, 29 Nov 2025 15:06:26 +0900 Subject: [PATCH 7/8] fix --- src/html/attributes.ts | 12 +++++++++-- src/html/index.ts | 2 +- src/html/tags.ts | 36 ++++++++++++++++----------------- src/html/util.ts | 16 ++++++++------- src/tsUtil.ts | 45 +++++++++++++++++++++++------------------- 5 files changed, 63 insertions(+), 48 deletions(-) diff --git a/src/html/attributes.ts b/src/html/attributes.ts index 51fc6bb..1a11144 100644 --- a/src/html/attributes.ts +++ b/src/html/attributes.ts @@ -77,7 +77,15 @@ export interface LinkResourceAttributes { download?: boolean | string; target?: '_blank' | '_self' | '_parent' | '_top' | string; hreflang?: string; - referrerpolicy?: 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'; + referrerpolicy?: + | 'no-referrer' + | 'no-referrer-when-downgrade' + | 'origin' + | 'origin-when-cross-origin' + | 'same-origin' + | 'strict-origin' + | 'strict-origin-when-cross-origin' + | 'unsafe-url'; } export interface MediaAttributes { @@ -132,4 +140,4 @@ export type AllHTMLAttributes = GlobalAttributes & TableAttributes & MetadataAttributes & ListAttributes & - EmbeddedAttributes; \ No newline at end of file + EmbeddedAttributes; diff --git a/src/html/index.ts b/src/html/index.ts index 2214dab..343e99f 100644 --- a/src/html/index.ts +++ b/src/html/index.ts @@ -1,3 +1,3 @@ export * from './attributes'; export * from './tags'; -export * from './util'; \ No newline at end of file +export * from './util'; diff --git a/src/html/tags.ts b/src/html/tags.ts index d2e70f8..fecead5 100644 --- a/src/html/tags.ts +++ b/src/html/tags.ts @@ -12,7 +12,7 @@ export type HTMLElement = declare const htmlBrand: unique symbol; export type Html< T extends HTMLElement[] | HTMLElement, - A extends AllHTMLAttributes = {} + A extends AllHTMLAttributes = {}, > = T extends HTMLElement[] ? { [htmlBrand]: 'html'; @@ -36,7 +36,7 @@ export type Html< declare const bodyBrand: unique symbol; export type Body< T extends HTMLElement[] | HTMLElement, - A extends AllHTMLAttributes = {} + A extends AllHTMLAttributes = {}, > = T extends Html ? never : T extends HTMLElement[] @@ -62,7 +62,7 @@ declare const divBrand: unique symbol; export type Div< T extends HTMLElement[] | HTMLElement, - A extends AllHTMLAttributes = {} + A extends AllHTMLAttributes = {}, > = T extends Html ? InvalidDivContent : T extends Body @@ -90,7 +90,7 @@ declare const pBrand: unique symbol; export type P< T extends HTMLElement[] | HTMLElement, - A extends AllHTMLAttributes = {} + A extends AllHTMLAttributes = {}, > = T extends Div ? InvalidPContent : T extends Html @@ -120,7 +120,7 @@ declare const h1Brand: unique symbol; export type H1< T extends HTMLElement[] | HTMLElement, - A extends AllHTMLAttributes = {} + A extends AllHTMLAttributes = {}, > = T extends Div ? InvalidH1Content : T extends Html @@ -152,7 +152,7 @@ declare const h2Brand: unique symbol; export type H2< T extends HTMLElement[] | HTMLElement, - A extends AllHTMLAttributes = {} + A extends AllHTMLAttributes = {}, > = T extends Div ? InvalidH2Content : T extends Html @@ -186,7 +186,7 @@ declare const h3Brand: unique symbol; export type H3< T extends HTMLElement[] | HTMLElement, - A extends AllHTMLAttributes = {} + A extends AllHTMLAttributes = {}, > = T extends Div ? InvalidH3Content : T extends Html @@ -222,7 +222,7 @@ declare const h4Brand: unique symbol; export type H4< T extends HTMLElement[] | HTMLElement, - A extends AllHTMLAttributes = {} + A extends AllHTMLAttributes = {}, > = T extends Div ? InvalidH4Content : T extends Html @@ -260,7 +260,7 @@ declare const h5Brand: unique symbol; export type H5< T extends HTMLElement[] | HTMLElement, - A extends AllHTMLAttributes = {} + A extends AllHTMLAttributes = {}, > = T extends Div ? InvalidH5Content : T extends Html @@ -293,12 +293,12 @@ export type H5< declare const imgBrand: unique symbol; -export type Img< - A extends AllHTMLAttributes = {} -> = { - [K in typeof imgBrand | 'children' | 'attributes']: - K extends typeof imgBrand ? 'img' - : K extends 'children' ? never - : K extends 'attributes' ? A - : never; -} \ No newline at end of file +export type Img = { + [K in typeof imgBrand | 'children' | 'attributes']: K extends typeof imgBrand + ? 'img' + : K extends 'children' + ? never + : K extends 'attributes' + ? A + : never; +}; diff --git a/src/html/util.ts b/src/html/util.ts index 42cde6f..4ad21a1 100644 --- a/src/html/util.ts +++ b/src/html/util.ts @@ -1,11 +1,13 @@ -const NON_CHILDREN_TAGS = ['img', 'link'] +const NON_CHILDREN_TAGS = ['img', 'link']; export type HtmlJson = { tag: string; children: (HtmlJson | string)[] | undefined; - attributes: { - key: string; - value: string; - }[] | undefined; + attributes: + | { + key: string; + value: string; + }[] + | undefined; }; export function renderToStream( @@ -14,7 +16,7 @@ export function renderToStream( indent: number = 0 ) { const space = ' '.repeat(indent * 2); - const attributes = input.attributes?.map((item) => `${item.key}="${item.value}"`).join(" "); + const attributes = input.attributes?.map((item) => `${item.key}="${item.value}"`).join(' '); if (input.children) { writeStream.write(`${space}<${input.tag}${attributes ? ` ${attributes}` : ''}>\n`); input.children?.forEach((item) => { @@ -28,4 +30,4 @@ export function renderToStream( } else { writeStream.write(`${space}<${input.tag}${attributes ? ` ${attributes}` : ''} />\n`); } -} \ No newline at end of file +} diff --git a/src/tsUtil.ts b/src/tsUtil.ts index 8759417..9d6e9d4 100644 --- a/src/tsUtil.ts +++ b/src/tsUtil.ts @@ -1,9 +1,9 @@ // TypeScript AST Processing Utilities -import {createWriteStream} from 'node:fs'; -import ts, {SyntaxKind} from 'typescript'; -import {type HtmlJson, renderToStream} from './html'; -import assert from "node:assert"; +import assert from 'node:assert'; +import { createWriteStream } from 'node:fs'; +import ts, { SyntaxKind } from 'typescript'; +import { type HtmlJson, renderToStream } from './html'; function parseAttributes(node: ts.Node | undefined): HtmlJson['attributes'] | undefined { if (node && ts.isTypeLiteralNode(node)) { @@ -15,9 +15,9 @@ function parseAttributes(node: ts.Node | undefined): HtmlJson['attributes'] | un ts.isLiteralTypeNode(obj.type) && ts.isStringLiteral(obj.type.literal) ) { - return {key: obj.name.escapedText.toString(), value: obj.type.literal.text} + return { key: obj.name.escapedText.toString(), value: obj.type.literal.text }; } else { - throw new Error('Invalid attribute was found in attributes.') + throw new Error('Invalid attribute was found in attributes.'); } }); } @@ -34,20 +34,22 @@ function parseChild(node: ts.Node, indent: number): HtmlJson | string { if (ts.isIdentifier(node.typeName)) { const tagName = node.typeName.escapedText.toString().toLowerCase(); // Parse attributes from type arguments if present - let attributes: { key: string, value: string }[] | undefined; + let attributes: { key: string; value: string }[] | undefined; if (node.typeArguments && node.typeArguments.length > 0) { attributes = parseAttributes(node.typeArguments[0]); } return { tag: tagName, children: undefined, - attributes + attributes, }; } - throw new Error(`unexpected type reference while parsing children elements.`) + throw new Error(`unexpected type reference while parsing children elements.`); } else { - console.log(node) - throw new Error(`unexpected type while parsing children elements. node kind is ${SyntaxKind[node.kind]}`) + console.log(node); + throw new Error( + `unexpected type while parsing children elements. node kind is ${SyntaxKind[node.kind]}` + ); } } @@ -55,7 +57,7 @@ function parseChildren(node: ts.Node, indent: number): HtmlJson['children'] { // node should be propertySignature assert(ts.isPropertySignature(node)); // type is not undefined - assert(node.type) + assert(node.type); if (ts.isTypeLiteralNode(node.type)) { return [traverseNode(node.type, indent + 2)]; } else if (ts.isLiteralTypeNode(node.type) && ts.isStringLiteral(node.type.literal)) { @@ -63,7 +65,7 @@ function parseChildren(node: ts.Node, indent: number): HtmlJson['children'] { } else if (ts.isTupleTypeNode(node.type)) { return node.type.elements.map((val) => parseChild(val, indent)); } else { - throw new Error("Unexpected error when parsing children."); + throw new Error('Unexpected error when parsing children.'); } } @@ -71,9 +73,13 @@ export function traverseNode(node: ts.Node, indent: number = 0): HtmlJson { if (ts.isTypeLiteralNode(node)) { let tag = ''; let children: (HtmlJson | string)[] | undefined = []; - let attributes: { key: string, value: string }[] | undefined; - node.members.forEach(member => { - if (ts.isPropertySignature(member) && ts.isComputedPropertyName(member.name) && ts.isIdentifier(member.name.expression)) { + let attributes: { key: string; value: string }[] | undefined; + node.members.forEach((member) => { + if ( + ts.isPropertySignature(member) && + ts.isComputedPropertyName(member.name) && + ts.isIdentifier(member.name.expression) + ) { tag = member.name.expression.escapedText.toString().replace('Brand', ''); } else if (ts.isPropertySignature(member) && ts.isIdentifier(member.name) && member.type) { switch (member.name.escapedText) { @@ -115,12 +121,11 @@ export function visit(node: ts.Node, checker: ts.TypeChecker, outPath: string) { const typeNode = checker.typeToTypeNode( type, undefined, - ts.NodeBuilderFlags.NoTruncation | - ts.NodeBuilderFlags.NoTypeReduction + ts.NodeBuilderFlags.NoTruncation | ts.NodeBuilderFlags.NoTypeReduction ); if (typeNode) { const result = traverseNode(typeNode, 0); - const writeStream = createWriteStream(outPath, {flags: 'w'}); + const writeStream = createWriteStream(outPath, { flags: 'w' }); renderToStream(result, writeStream); writeStream.end('\n'); } @@ -163,7 +168,7 @@ export function processTypeScript(filePath: string, outPath: string): boolean { return false; } - const {sourceFile, checker} = result; + const { sourceFile, checker } = result; try { visit(sourceFile, checker, outPath); From c061f4473c10dddb02d2925ec48352a5b9f27bc8 Mon Sep 17 00:00:00 2001 From: jiko21 Date: Sat, 29 Nov 2025 15:07:18 +0900 Subject: [PATCH 8/8] obey lint --- src/html/util.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/html/util.ts b/src/html/util.ts index 4ad21a1..1729ff9 100644 --- a/src/html/util.ts +++ b/src/html/util.ts @@ -1,4 +1,3 @@ -const NON_CHILDREN_TAGS = ['img', 'link']; export type HtmlJson = { tag: string; children: (HtmlJson | string)[] | undefined;