@lonnycorp/htmlforge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/chunk-D7D4PA-g.mjs +13 -0
- package/dist/index.d.mts +143 -0
- package/dist/index.mjs +329 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Tim Lonsdale
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# HTMLForge
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
A minimal, zero-dependency library for building fully-styled HTML in TypeScript/JavaScript.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- Zero dependencies.
|
|
11
|
+
- Efficient and ergonomic inline styling (using de-duplicated dynamic classes).
|
|
12
|
+
- Reusable "Component"-pattern for composing common UIs.
|
|
13
|
+
|
|
14
|
+
## Quick Look
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { Document, node } from "@lonnycorp/htmlforge"
|
|
18
|
+
|
|
19
|
+
const html = new Document()
|
|
20
|
+
html.attribute("lang", "en-GB")
|
|
21
|
+
html.head.child(
|
|
22
|
+
new node.Element("title").child(
|
|
23
|
+
new node.Text("Acme Title")
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
html.body.child(
|
|
28
|
+
new node.Element("div")
|
|
29
|
+
.style("width", "100%")
|
|
30
|
+
.style("background-color", "blue")
|
|
31
|
+
.style("background-color", "red", { pseudoSelector: ":hover" })
|
|
32
|
+
.child(new node.Text("Hello world"))
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const validHTML = html.toString()
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
Install the package from npm:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @lonnycorp/htmlforge
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
### HTML structure
|
|
49
|
+
|
|
50
|
+
An HTMLForge `Document` instance is a tree of nodes. Nodes come in a few flavors:
|
|
51
|
+
- `node.Element`: represents tags (e.g., `<div>`, `<span>`), can hold attributes and inline styles, and can nest any other node as a child.
|
|
52
|
+
- `node.Text`: holds plain text content that will be HTML-escaped.
|
|
53
|
+
- `node.Raw`: holds raw HTML without escaping.
|
|
54
|
+
- `node.Fragment`: groups a collection of child nodes without introducing a wrapping element.
|
|
55
|
+
|
|
56
|
+
### Creating an HTML document
|
|
57
|
+
|
|
58
|
+
Create a new HTML document using `new Document()`, set attributes on the root `<html>` element via `html.attribute`, and work directly with `html.head` and `html.body` to populate content. The constructor accepts optional parameters:
|
|
59
|
+
- `displaySignature` (default `true`): whether to include the `<!-- Generated by HTMLForge -->` signature comment.
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { Document, node } from "@lonnycorp/htmlforge"
|
|
63
|
+
|
|
64
|
+
const html = new Document({ displaySignature: false })
|
|
65
|
+
.attribute("lang", "en")
|
|
66
|
+
.attribute("data-theme", "dark")
|
|
67
|
+
|
|
68
|
+
html.body
|
|
69
|
+
.style("margin", "auto")
|
|
70
|
+
.child(new node.Text("Hello world"))
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## `node.Element` nodes
|
|
74
|
+
|
|
75
|
+
`node.Element` supports:
|
|
76
|
+
- `attribute(name, value)` for HTML attributes
|
|
77
|
+
- `style(property, value, options?)` for inline styles (with optional `pseudoSelector`, `mediaQuery` parameters)
|
|
78
|
+
- `child(node)` to nest children nodes
|
|
79
|
+
|
|
80
|
+
These calls are chainable to keep element construction compact.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { node } from "@lonnycorp/htmlforge"
|
|
84
|
+
|
|
85
|
+
const card = new node.Element("section")
|
|
86
|
+
.attribute("aria-label", "profile card")
|
|
87
|
+
.style("border", "1px solid #ccc")
|
|
88
|
+
.child(
|
|
89
|
+
new node.Element("h2").child(new node.Text("Ada Lovelace"))
|
|
90
|
+
)
|
|
91
|
+
.child(
|
|
92
|
+
new node.Element("p")
|
|
93
|
+
.style("color", "#555")
|
|
94
|
+
.child(new node.Text("First computer programmer."))
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### `node.Fragment` nodes
|
|
99
|
+
|
|
100
|
+
`node.Fragment` groups child nodes without adding a wrapper element. It only supports `child` (also chainable).
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { node } from "@lonnycorp/htmlforge"
|
|
104
|
+
|
|
105
|
+
const listItems = new node.Fragment()
|
|
106
|
+
.child(new node.Element("li").child(new node.Text("One")))
|
|
107
|
+
.child(new node.Element("li").child(new node.Text("Two")))
|
|
108
|
+
.child(new node.Element("li").child(new node.Text("Three")))
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Text and Raw nodes
|
|
112
|
+
|
|
113
|
+
- `node.Text` holds HTML-escaped text content (no additional methods).
|
|
114
|
+
- `node.Raw` injects raw HTML as-is (no additional methods).
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { node } from "@lonnycorp/htmlforge"
|
|
118
|
+
|
|
119
|
+
const safeText = new node.Text("<em>Escaped</em> output")
|
|
120
|
+
const rawHtml = new node.Raw("<em>Unescaped</em> output")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Define your own nodes
|
|
124
|
+
|
|
125
|
+
Implement the `node.Buildable` interface to build reusable components. Compose a private `node.Element` (style/shape it however you like) and proxy its `build()` method. Anything that implements `node.Buildable` can be passed to `child` on `node.Element` or `node.Fragment`.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { node } from "@lonnycorp/htmlforge"
|
|
129
|
+
|
|
130
|
+
class Alert implements node.Buildable {
|
|
131
|
+
private readonly el = new node.Element("div")
|
|
132
|
+
.attribute("role", "alert")
|
|
133
|
+
.style("padding", "12px 16px")
|
|
134
|
+
.style("background-color", "#fffae6")
|
|
135
|
+
|
|
136
|
+
constructor(message: string) {
|
|
137
|
+
this.el.child(new node.Text(message))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Optional: expose child to let callers inject arbitrary child nodes
|
|
141
|
+
child(child: node.Buildable) {
|
|
142
|
+
this.el.child(child)
|
|
143
|
+
return this
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
build() {
|
|
147
|
+
return this.el.build()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
//#region src/artifact/raw.d.ts
|
|
2
|
+
type Raw$1 = {
|
|
3
|
+
buildArtifactType: "RAW";
|
|
4
|
+
raw: string;
|
|
5
|
+
};
|
|
6
|
+
declare namespace style_d_exports {
|
|
7
|
+
export { MediaQuery, PseudoSelector, Style };
|
|
8
|
+
}
|
|
9
|
+
type PseudoSelector = `:${string}`;
|
|
10
|
+
type MediaQuery = `@media${string}`;
|
|
11
|
+
type Style = {
|
|
12
|
+
name: string;
|
|
13
|
+
value: string;
|
|
14
|
+
pseudoSelector: PseudoSelector | null;
|
|
15
|
+
mediaQuery: MediaQuery | null;
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/artifact/styled-class.d.ts
|
|
19
|
+
type StyledClass = {
|
|
20
|
+
buildArtifactType: "STYLED_CLASS";
|
|
21
|
+
styles: Style[];
|
|
22
|
+
className: string;
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/artifact/tag-close.d.ts
|
|
26
|
+
type TagClose = {
|
|
27
|
+
buildArtifactType: "TAG_CLOSE";
|
|
28
|
+
tagName: string;
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/node/buildable.d.ts
|
|
32
|
+
interface Buildable {
|
|
33
|
+
build(): Iterable<Artifact>;
|
|
34
|
+
}
|
|
35
|
+
declare namespace element_d_exports {
|
|
36
|
+
export { Attribute, Element };
|
|
37
|
+
}
|
|
38
|
+
type Attribute = {
|
|
39
|
+
name: string;
|
|
40
|
+
value: string;
|
|
41
|
+
};
|
|
42
|
+
declare class Element implements Buildable {
|
|
43
|
+
private readonly tagName;
|
|
44
|
+
private readonly attributes;
|
|
45
|
+
private readonly styles;
|
|
46
|
+
private readonly children;
|
|
47
|
+
constructor(tagName: string);
|
|
48
|
+
style(name: string, value: string, options?: {
|
|
49
|
+
mediaQuery?: MediaQuery;
|
|
50
|
+
pseudoSelector?: PseudoSelector;
|
|
51
|
+
}): this;
|
|
52
|
+
attribute(name: string, value: string): this;
|
|
53
|
+
child(node: Buildable): this;
|
|
54
|
+
build(): Iterable<Artifact>;
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/artifact/tag-open.d.ts
|
|
58
|
+
type TagOpen = {
|
|
59
|
+
buildArtifactType: "TAG_OPEN";
|
|
60
|
+
tagName: string;
|
|
61
|
+
isVoid: boolean;
|
|
62
|
+
attributes: Attribute[];
|
|
63
|
+
};
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/artifact/text.d.ts
|
|
66
|
+
type Text$1 = {
|
|
67
|
+
buildArtifactType: "TEXT";
|
|
68
|
+
text: string;
|
|
69
|
+
};
|
|
70
|
+
declare namespace index_d_exports {
|
|
71
|
+
export { Artifact, Raw$1 as Raw, StyledClass, TagClose, TagOpen, Text$1 as Text, render };
|
|
72
|
+
}
|
|
73
|
+
type Artifact = TagOpen | TagClose | Text$1 | Raw$1 | StyledClass;
|
|
74
|
+
declare const render: (artifact: Artifact, fragments: string[]) => void;
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/node/doctype.d.ts
|
|
77
|
+
declare class Doctype implements Buildable {
|
|
78
|
+
private readonly raw;
|
|
79
|
+
constructor();
|
|
80
|
+
build(): Iterable<Artifact>;
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/node/font-face.d.ts
|
|
84
|
+
type FontFaceOptions = {
|
|
85
|
+
display?: string;
|
|
86
|
+
format?: string;
|
|
87
|
+
style?: string;
|
|
88
|
+
weight?: string;
|
|
89
|
+
};
|
|
90
|
+
declare class FontFace implements Buildable {
|
|
91
|
+
private readonly name;
|
|
92
|
+
private readonly url;
|
|
93
|
+
private readonly options;
|
|
94
|
+
constructor(name: string, url: string, options?: FontFaceOptions);
|
|
95
|
+
build(): Iterable<Artifact>;
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/node/fragment.d.ts
|
|
99
|
+
declare class Fragment implements Buildable {
|
|
100
|
+
private readonly children;
|
|
101
|
+
constructor();
|
|
102
|
+
child(node: Buildable): this;
|
|
103
|
+
build(): Iterable<Artifact>;
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/node/raw.d.ts
|
|
107
|
+
declare class Raw implements Buildable {
|
|
108
|
+
private readonly raw;
|
|
109
|
+
constructor(raw: string);
|
|
110
|
+
build(): Iterable<Artifact>;
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/node/signature.d.ts
|
|
114
|
+
declare class Signature implements Buildable {
|
|
115
|
+
private readonly raw;
|
|
116
|
+
constructor();
|
|
117
|
+
build(): Iterable<Artifact>;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/node/text.d.ts
|
|
121
|
+
declare class Text implements Buildable {
|
|
122
|
+
private readonly text;
|
|
123
|
+
constructor(text: string);
|
|
124
|
+
build(): Iterable<Artifact>;
|
|
125
|
+
}
|
|
126
|
+
declare namespace index_d_exports$1 {
|
|
127
|
+
export { Buildable, Doctype, Element, FontFace, FontFaceOptions, Fragment, Raw, Signature, Text, element_d_exports as element };
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/document.d.ts
|
|
131
|
+
declare class Document {
|
|
132
|
+
private readonly root;
|
|
133
|
+
private readonly html;
|
|
134
|
+
readonly head: Element;
|
|
135
|
+
readonly body: Element;
|
|
136
|
+
constructor(params?: {
|
|
137
|
+
displaySignature?: boolean;
|
|
138
|
+
});
|
|
139
|
+
attribute(name: string, value: string): this;
|
|
140
|
+
toString(): string;
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
export { Document, type Style, index_d_exports as artifact, index_d_exports$1 as node, style_d_exports as style };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-D7D4PA-g.mjs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
//#region src/artifact/raw.ts
|
|
4
|
+
const render$5 = (artifact, fragments) => {
|
|
5
|
+
fragments.push(artifact.raw);
|
|
6
|
+
};
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/util/escape.ts
|
|
9
|
+
const escapeHtml = (value) => {
|
|
10
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/util/group-by.ts
|
|
14
|
+
const groupBy = (items, getKey) => {
|
|
15
|
+
const map = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const item of items) {
|
|
17
|
+
const key = getKey(item);
|
|
18
|
+
const group = map.get(key);
|
|
19
|
+
if (group) group.push(item);
|
|
20
|
+
else map.set(key, [item]);
|
|
21
|
+
}
|
|
22
|
+
return map;
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/artifact/styled-class.ts
|
|
26
|
+
const render$4 = (artifact, fragments) => {
|
|
27
|
+
const groupedStyles = groupBy(artifact.styles, (s) => JSON.stringify([s.mediaQuery, s.pseudoSelector]));
|
|
28
|
+
for (const styles of groupedStyles.values()) {
|
|
29
|
+
const { mediaQuery, pseudoSelector } = styles[0];
|
|
30
|
+
if (mediaQuery) fragments.push(`${mediaQuery} {`);
|
|
31
|
+
const selector = pseudoSelector ? `.${artifact.className}${pseudoSelector}` : `.${artifact.className}`;
|
|
32
|
+
fragments.push(`${selector} {`);
|
|
33
|
+
for (const style of styles) fragments.push(`${style.name}: ${style.value};`);
|
|
34
|
+
fragments.push("}");
|
|
35
|
+
if (mediaQuery) fragments.push("}");
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/artifact/tag-close.ts
|
|
40
|
+
const render$3 = (artifact, fragments) => {
|
|
41
|
+
fragments.push(`</${artifact.tagName}>`);
|
|
42
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/artifact/tag-open.ts
|
|
45
|
+
const render$2 = (artifact, fragments) => {
|
|
46
|
+
fragments.push(`<${artifact.tagName}`);
|
|
47
|
+
const groupedAttributes = groupBy(artifact.attributes, (attribute) => attribute.name);
|
|
48
|
+
for (const [name, attributes] of groupedAttributes) {
|
|
49
|
+
const lastAttribute = attributes[attributes.length - 1];
|
|
50
|
+
if (!lastAttribute) continue;
|
|
51
|
+
const value = name === "class" ? attributes.map((attribute) => attribute.value).join(" ") : lastAttribute.value;
|
|
52
|
+
fragments.push(` ${name}="${escapeHtml(value)}"`);
|
|
53
|
+
}
|
|
54
|
+
fragments.push(">");
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/artifact/text.ts
|
|
58
|
+
const render$1 = (artifact, fragments) => {
|
|
59
|
+
fragments.push(escapeHtml(artifact.text));
|
|
60
|
+
};
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/artifact/index.ts
|
|
63
|
+
var artifact_exports = /* @__PURE__ */ __exportAll({ render: () => render });
|
|
64
|
+
const render = (artifact, fragments) => {
|
|
65
|
+
if (artifact.buildArtifactType === "TAG_OPEN") render$2(artifact, fragments);
|
|
66
|
+
else if (artifact.buildArtifactType === "TAG_CLOSE") render$3(artifact, fragments);
|
|
67
|
+
else if (artifact.buildArtifactType === "TEXT") render$1(artifact, fragments);
|
|
68
|
+
else if (artifact.buildArtifactType === "RAW") render$5(artifact, fragments);
|
|
69
|
+
else if (artifact.buildArtifactType === "STYLED_CLASS") render$4(artifact, fragments);
|
|
70
|
+
else throw new Error("Invariant: Invalid artifact type");
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/node/raw.ts
|
|
74
|
+
var Raw = class {
|
|
75
|
+
raw;
|
|
76
|
+
constructor(raw) {
|
|
77
|
+
this.raw = raw;
|
|
78
|
+
}
|
|
79
|
+
*build() {
|
|
80
|
+
yield {
|
|
81
|
+
buildArtifactType: "RAW",
|
|
82
|
+
raw: this.raw
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/node/doctype.ts
|
|
88
|
+
const RAW$1 = "<!DOCTYPE html>";
|
|
89
|
+
var Doctype = class {
|
|
90
|
+
raw;
|
|
91
|
+
constructor() {
|
|
92
|
+
this.raw = new Raw(RAW$1);
|
|
93
|
+
}
|
|
94
|
+
build() {
|
|
95
|
+
return this.raw.build();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/node/element.ts
|
|
100
|
+
var element_exports = /* @__PURE__ */ __exportAll({ Element: () => Element });
|
|
101
|
+
const VOID_ELEMENTS = new Set([
|
|
102
|
+
"area",
|
|
103
|
+
"base",
|
|
104
|
+
"br",
|
|
105
|
+
"col",
|
|
106
|
+
"embed",
|
|
107
|
+
"hr",
|
|
108
|
+
"img",
|
|
109
|
+
"input",
|
|
110
|
+
"link",
|
|
111
|
+
"meta",
|
|
112
|
+
"param",
|
|
113
|
+
"source",
|
|
114
|
+
"track",
|
|
115
|
+
"wbr"
|
|
116
|
+
]);
|
|
117
|
+
var Element = class {
|
|
118
|
+
tagName;
|
|
119
|
+
attributes;
|
|
120
|
+
styles;
|
|
121
|
+
children;
|
|
122
|
+
constructor(tagName) {
|
|
123
|
+
this.tagName = tagName.toLowerCase();
|
|
124
|
+
this.attributes = [];
|
|
125
|
+
this.styles = [];
|
|
126
|
+
this.children = [];
|
|
127
|
+
}
|
|
128
|
+
style(name, value, options) {
|
|
129
|
+
const pseudoSelector = options?.pseudoSelector ?? null;
|
|
130
|
+
const mediaQuery = options?.mediaQuery ?? null;
|
|
131
|
+
this.styles.push({
|
|
132
|
+
name,
|
|
133
|
+
value,
|
|
134
|
+
pseudoSelector,
|
|
135
|
+
mediaQuery
|
|
136
|
+
});
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
attribute(name, value) {
|
|
140
|
+
this.attributes.push({
|
|
141
|
+
name,
|
|
142
|
+
value
|
|
143
|
+
});
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
child(node) {
|
|
147
|
+
this.children.push(node);
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
*build() {
|
|
151
|
+
const attributes = [...this.attributes];
|
|
152
|
+
const isVoid = VOID_ELEMENTS.has(this.tagName);
|
|
153
|
+
if (this.styles.length > 0) {
|
|
154
|
+
const hash = createHash("sha256");
|
|
155
|
+
for (const style of this.styles) hash.update(JSON.stringify(style));
|
|
156
|
+
const className = `f${hash.digest().subarray(0, 8).toString("base64url")}`;
|
|
157
|
+
attributes.push({
|
|
158
|
+
name: "class",
|
|
159
|
+
value: className
|
|
160
|
+
});
|
|
161
|
+
yield {
|
|
162
|
+
buildArtifactType: "STYLED_CLASS",
|
|
163
|
+
styles: [...this.styles],
|
|
164
|
+
className
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
yield {
|
|
168
|
+
buildArtifactType: "TAG_OPEN",
|
|
169
|
+
tagName: this.tagName,
|
|
170
|
+
isVoid,
|
|
171
|
+
attributes
|
|
172
|
+
};
|
|
173
|
+
if (isVoid) return;
|
|
174
|
+
for (const child of this.children) yield* child.build();
|
|
175
|
+
yield {
|
|
176
|
+
buildArtifactType: "TAG_CLOSE",
|
|
177
|
+
tagName: this.tagName
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/node/font-face.ts
|
|
183
|
+
var FontFace = class {
|
|
184
|
+
name;
|
|
185
|
+
url;
|
|
186
|
+
options;
|
|
187
|
+
constructor(name, url, options) {
|
|
188
|
+
this.name = name;
|
|
189
|
+
this.url = url;
|
|
190
|
+
this.options = options ?? {};
|
|
191
|
+
}
|
|
192
|
+
*build() {
|
|
193
|
+
yield {
|
|
194
|
+
buildArtifactType: "TAG_OPEN",
|
|
195
|
+
tagName: "style",
|
|
196
|
+
isVoid: false,
|
|
197
|
+
attributes: []
|
|
198
|
+
};
|
|
199
|
+
yield {
|
|
200
|
+
buildArtifactType: "RAW",
|
|
201
|
+
raw: "@font-face {"
|
|
202
|
+
};
|
|
203
|
+
yield {
|
|
204
|
+
buildArtifactType: "RAW",
|
|
205
|
+
raw: `font-family: ${this.name};`
|
|
206
|
+
};
|
|
207
|
+
yield {
|
|
208
|
+
buildArtifactType: "RAW",
|
|
209
|
+
raw: this.options.format ? `src: url("${this.url}") format("${this.options.format}");` : `src: url("${this.url}");`
|
|
210
|
+
};
|
|
211
|
+
if (this.options.display) yield {
|
|
212
|
+
buildArtifactType: "RAW",
|
|
213
|
+
raw: `font-display: ${this.options.display};`
|
|
214
|
+
};
|
|
215
|
+
if (this.options.style) yield {
|
|
216
|
+
buildArtifactType: "RAW",
|
|
217
|
+
raw: `font-style: ${this.options.style};`
|
|
218
|
+
};
|
|
219
|
+
if (this.options.weight) yield {
|
|
220
|
+
buildArtifactType: "RAW",
|
|
221
|
+
raw: `font-weight: ${this.options.weight};`
|
|
222
|
+
};
|
|
223
|
+
yield {
|
|
224
|
+
buildArtifactType: "RAW",
|
|
225
|
+
raw: "}"
|
|
226
|
+
};
|
|
227
|
+
yield {
|
|
228
|
+
buildArtifactType: "TAG_CLOSE",
|
|
229
|
+
tagName: "style"
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/node/fragment.ts
|
|
235
|
+
var Fragment = class {
|
|
236
|
+
children;
|
|
237
|
+
constructor() {
|
|
238
|
+
this.children = [];
|
|
239
|
+
}
|
|
240
|
+
child(node) {
|
|
241
|
+
this.children.push(node);
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
*build() {
|
|
245
|
+
for (const child of this.children) yield* child.build();
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/node/signature.ts
|
|
250
|
+
const RAW = "<!-- Generated by HTMLForge (https://github.com/lonnycorp/htmlforge) -->";
|
|
251
|
+
var Signature = class {
|
|
252
|
+
raw;
|
|
253
|
+
constructor() {
|
|
254
|
+
this.raw = new Raw(RAW);
|
|
255
|
+
}
|
|
256
|
+
build() {
|
|
257
|
+
return this.raw.build();
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/node/text.ts
|
|
262
|
+
var Text = class {
|
|
263
|
+
text;
|
|
264
|
+
constructor(text) {
|
|
265
|
+
this.text = text;
|
|
266
|
+
}
|
|
267
|
+
*build() {
|
|
268
|
+
yield {
|
|
269
|
+
buildArtifactType: "TEXT",
|
|
270
|
+
text: this.text
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/node/index.ts
|
|
276
|
+
var node_exports = /* @__PURE__ */ __exportAll({
|
|
277
|
+
Doctype: () => Doctype,
|
|
278
|
+
Element: () => Element,
|
|
279
|
+
FontFace: () => FontFace,
|
|
280
|
+
Fragment: () => Fragment,
|
|
281
|
+
Raw: () => Raw,
|
|
282
|
+
Signature: () => Signature,
|
|
283
|
+
Text: () => Text,
|
|
284
|
+
element: () => element_exports
|
|
285
|
+
});
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region src/style.ts
|
|
288
|
+
var style_exports = /* @__PURE__ */ __exportAll({});
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/document.ts
|
|
291
|
+
const DEFAULT_SIGNATURE_DISPLAY = true;
|
|
292
|
+
var Document = class {
|
|
293
|
+
root;
|
|
294
|
+
html;
|
|
295
|
+
head;
|
|
296
|
+
body;
|
|
297
|
+
constructor(params) {
|
|
298
|
+
this.head = new Element("head").child(new Element("style"));
|
|
299
|
+
this.body = new Element("body");
|
|
300
|
+
this.html = new Element("html").child(this.head).child(this.body);
|
|
301
|
+
this.root = new Fragment().child(new Doctype());
|
|
302
|
+
if (params?.displaySignature ?? DEFAULT_SIGNATURE_DISPLAY) this.root.child(new Signature());
|
|
303
|
+
this.root.child(this.html);
|
|
304
|
+
}
|
|
305
|
+
attribute(name, value) {
|
|
306
|
+
this.html.attribute(name, value);
|
|
307
|
+
return this;
|
|
308
|
+
}
|
|
309
|
+
toString() {
|
|
310
|
+
const artifactBuffer = [];
|
|
311
|
+
const styledClassArtifacts = [];
|
|
312
|
+
const seenStyledClasses = /* @__PURE__ */ new Set();
|
|
313
|
+
const fragments = [];
|
|
314
|
+
let spliceIndex = -1;
|
|
315
|
+
for (const artifact of this.root.build()) {
|
|
316
|
+
if (artifact.buildArtifactType === "STYLED_CLASS") {
|
|
317
|
+
if (seenStyledClasses.has(artifact.className)) continue;
|
|
318
|
+
seenStyledClasses.add(artifact.className);
|
|
319
|
+
styledClassArtifacts.push(artifact);
|
|
320
|
+
} else artifactBuffer.push(artifact);
|
|
321
|
+
if (spliceIndex === -1 && artifact.buildArtifactType === "TAG_OPEN" && artifact.tagName === "style") spliceIndex = artifactBuffer.length;
|
|
322
|
+
}
|
|
323
|
+
artifactBuffer.splice(spliceIndex, 0, ...styledClassArtifacts);
|
|
324
|
+
for (const artifact of artifactBuffer) render(artifact, fragments);
|
|
325
|
+
return fragments.join("");
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
//#endregion
|
|
329
|
+
export { Document, artifact_exports as artifact, node_exports as node, style_exports as style };
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lonnycorp/htmlforge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "bun build.ts",
|
|
7
|
+
"check": "bun run typecheck && bun run lint && bun test",
|
|
8
|
+
"lint": "eslint --max-warnings=0",
|
|
9
|
+
"typecheck": "tsc --noEmit",
|
|
10
|
+
"tsdown": "tsdown"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@stylistic/eslint-plugin": "^5.5.0",
|
|
14
|
+
"@types/bun": "latest",
|
|
15
|
+
"@typescript-eslint/eslint-plugin": "^8.46.3",
|
|
16
|
+
"@typescript-eslint/parser": "^8.46.3",
|
|
17
|
+
"eslint": "^9.39.1",
|
|
18
|
+
"tsdown": "^0.22.2",
|
|
19
|
+
"typedoc": "^0.28.15",
|
|
20
|
+
"typescript": "^5"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"url": "git+https://github.com/lonnycorp/htmlforge.git"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.mts",
|
|
31
|
+
"import": "./dist/index.mjs"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"type": "module"
|
|
38
|
+
}
|