Skip to content

Commit 3a5a0a9

Browse files
Use objects to represent html markup (#464)
Co-authored-by: josh-hemphill <[email protected]>
1 parent 8560048 commit 3a5a0a9

27 files changed

+6222
-64
lines changed

src/index.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,21 @@ export const config = {
2121

2222
export type FaviconHtmlElement = string;
2323

24+
export interface FaviconHtmlTag {
25+
readonly tag: string;
26+
readonly attrs: Record<string, string | boolean>;
27+
}
28+
2429
export interface FaviconResponse {
2530
readonly images: FaviconImage[];
2631
readonly files: FaviconFile[];
2732
readonly html: FaviconHtmlElement[];
33+
readonly htmlTags: FaviconHtmlTag[];
2834
}
2935

36+
export type FaviconsSource = string | Buffer | (string | Buffer)[];
3037
export async function favicons(
31-
source: string | Buffer | (string | Buffer)[],
38+
source: FaviconsSource,
3239
options: FaviconOptions = {},
3340
): Promise<FaviconResponse> {
3441
options = {
@@ -60,6 +67,7 @@ export async function favicons(
6067
images: responses.flatMap((r) => r.images),
6168
files: responses.flatMap((r) => r.files),
6269
html: responses.flatMap((r) => r.html),
70+
htmlTags: responses.flatMap((r) => r.htmlTags),
6371
};
6472
}
6573

@@ -71,7 +79,10 @@ export interface FaviconStreamOptions extends FaviconOptions {
7179
readonly emitBuffers?: boolean;
7280
}
7381

74-
export type HandleHTML = (html: FaviconHtmlElement[]) => void;
82+
export type HandleHTML = (
83+
html: FaviconHtmlElement[],
84+
htmlTags: FaviconHtmlTag[],
85+
) => void;
7586

7687
class FaviconStream extends Transform {
7788
#options: FaviconStreamOptions;
@@ -91,7 +102,7 @@ class FaviconStream extends Transform {
91102
const { html: htmlPath, pipeHTML, ...options } = this.#options;
92103

93104
favicons(file, options)
94-
.then(({ images, files, html }) => {
105+
.then(({ images, files, html, htmlTags }) => {
95106
for (const { name, contents } of [...images, ...files]) {
96107
this.push({
97108
name,
@@ -100,7 +111,7 @@ class FaviconStream extends Transform {
100111
}
101112

102113
if (this.#handleHTML) {
103-
this.#handleHTML(html);
114+
this.#handleHTML(html, htmlTags);
104115
}
105116

106117
if (pipeHTML) {

src/platforms/android.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import escapeHtml from "escape-html";
2-
import { FaviconFile, FaviconHtmlElement, FaviconImage } from "../index";
1+
import { FaviconHtmlTag, FaviconFile, FaviconImage } from "../index";
32
import {
43
FaviconOptions,
54
IconOptions,
@@ -136,17 +135,39 @@ export class AndroidPlatform extends Platform {
136135
return [this.manifest()];
137136
}
138137

139-
override async createHtml(): Promise<FaviconHtmlElement[]> {
140-
// prettier-ignore
138+
override async createHtml(): Promise<FaviconHtmlTag[]> {
141139
return [
142-
this.options.loadManifestWithCredentials
143-
? `<link rel="manifest" href="${this.cacheBusting(this.relative(this.manifestFileName()))}" crossOrigin="use-credentials">`
144-
: `<link rel="manifest" href="${this.cacheBusting(this.relative(this.manifestFileName()))}">`,
145-
`<meta name="mobile-web-app-capable" content="yes">`,
146-
`<meta name="theme-color" content="${this.options.theme_color || this.options.background}">`,
147-
this.options.appName
148-
? `<meta name="application-name" content="${escapeHtml(this.options.appName)}">`
149-
: `<meta name="application-name">`,
140+
{
141+
tag: "link",
142+
attrs: {
143+
rel: "manifest",
144+
href: this.cacheBusting(this.relative(this.manifestFileName())),
145+
crossOrigin: this.options.loadManifestWithCredentials
146+
? "use-credentials"
147+
: false,
148+
},
149+
},
150+
{
151+
tag: "meta",
152+
attrs: {
153+
name: "mobile-web-app-capable",
154+
content: "yes",
155+
},
156+
},
157+
{
158+
tag: "meta",
159+
attrs: {
160+
name: "theme-color",
161+
content: this.options.theme_color || this.options.background,
162+
},
163+
},
164+
{
165+
tag: "meta",
166+
attrs: {
167+
name: "application-name",
168+
content: this.options.appName || false,
169+
},
170+
},
150171
];
151172
}
152173

src/platforms/appleIcon.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import escapeHtml from "escape-html";
2-
import { FaviconHtmlElement } from "../index";
1+
import { FaviconHtmlTag } from "../index";
32
import { FaviconOptions, NamedIconOptions } from "../config/defaults";
43
import { opaqueIcon } from "../config/icons";
54
import { Platform, uniformIconOptions } from "./base";
@@ -28,26 +27,46 @@ export class AppleIconPlatform extends Platform {
2827
);
2928
}
3029

31-
override async createHtml(): Promise<FaviconHtmlElement[]> {
30+
override async createHtml(): Promise<FaviconHtmlTag[]> {
3231
const icons = this.iconOptions
3332
.filter(({ name }) => /\d/.test(name)) // with a size in a name
3433
.map((options) => {
3534
const { width, height } = options.sizes[0];
36-
37-
// prettier-ignore
38-
return `<link rel="apple-touch-icon" sizes="${width}x${height}" href="${this.cacheBusting(this.relative(options.name))}">`;
35+
return {
36+
tag: "link",
37+
attrs: {
38+
rel: "apple-touch-icon",
39+
sizes: `${width}x${height}`,
40+
href: this.cacheBusting(this.relative(options.name)),
41+
},
42+
};
3943
});
4044

4145
const name = this.options.appShortName || this.options.appName;
4246

43-
// prettier-ignore
4447
return [
4548
...icons,
46-
`<meta name="apple-mobile-web-app-capable" content="yes">`,
47-
`<meta name="apple-mobile-web-app-status-bar-style" content="${this.options.appleStatusBarStyle}">`,
48-
name
49-
? `<meta name="apple-mobile-web-app-title" content="${escapeHtml(name)}">`
50-
: `<meta name="apple-mobile-web-app-title">`
49+
{
50+
tag: "meta",
51+
attrs: {
52+
name: "apple-mobile-web-app-capable",
53+
content: "yes",
54+
},
55+
},
56+
{
57+
tag: "meta",
58+
attrs: {
59+
name: "apple-mobile-web-app-status-bar-style",
60+
content: this.options.appleStatusBarStyle,
61+
},
62+
},
63+
{
64+
tag: "meta",
65+
attrs: {
66+
name: "apple-mobile-web-app-title",
67+
content: name || false,
68+
},
69+
},
5170
];
5271
}
5372
}

src/platforms/appleStartup.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FaviconHtmlElement } from "../index";
1+
import { FaviconHtmlTag } from "../index";
22
import { FaviconOptions, NamedIconOptions } from "../config/defaults";
33
import { opaqueIcon } from "../config/icons";
44
import { Platform, uniformIconOptions } from "./base";
@@ -70,10 +70,14 @@ export class AppleStartupPlatform extends Platform<AppleStartupImage> {
7070
);
7171
}
7272

73-
override async createHtml(): Promise<FaviconHtmlElement[]> {
74-
// prettier-ignore
75-
return this.iconOptions.map((item) =>
76-
`<link rel="apple-touch-startup-image" media="(device-width: ${item.deviceWidth}px) and (device-height: ${item.deviceHeight}px) and (-webkit-device-pixel-ratio: ${item.pixelRatio}) and (orientation: ${item.orientation})" href="${this.cacheBusting(this.relative(item.name))}">`
77-
);
73+
override async createHtml(): Promise<FaviconHtmlTag[]> {
74+
return this.iconOptions.map((item) => ({
75+
tag: "link",
76+
attrs: {
77+
rel: "apple-touch-startup-image",
78+
media: `(device-width: ${item.deviceWidth}px) and (device-height: ${item.deviceHeight}px) and (-webkit-device-pixel-ratio: ${item.pixelRatio}) and (orientation: ${item.orientation})`,
79+
href: this.cacheBusting(this.relative(item.name)),
80+
},
81+
}));
7882
}
7983
}

src/platforms/base.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import escapeHTML from "escape-html";
12
import {
23
FaviconFile,
3-
FaviconHtmlElement,
4+
FaviconHtmlTag,
45
FaviconImage,
56
FaviconResponse,
67
} from "../index";
@@ -60,6 +61,26 @@ export function uniformIconOptions<T extends NamedIconOptions>(
6061
}));
6162
}
6263

64+
function attrSorkKey(key: string): string {
65+
const attrs = ["name", "rel", "type", "media", "sizes"];
66+
const index = attrs.indexOf(key);
67+
return index >= 0 ? `${index}_${key}` : `z_${key}`;
68+
}
69+
70+
function renderHtmlTag(tag: FaviconHtmlTag): string {
71+
const attrs = Object.entries(tag.attrs)
72+
.toSorted((a, b) => attrSorkKey(a[0]).localeCompare(attrSorkKey(b[0])))
73+
.map(([key, value]) => {
74+
if (value === true) return key;
75+
if (value === false) return "";
76+
return `${key}="${escapeHTML(value)}"`;
77+
})
78+
.filter(Boolean)
79+
.join(" ");
80+
81+
return `<${tag.tag} ${attrs || ""}>`;
82+
}
83+
6384
export class Platform<IO extends NamedIconOptions = NamedIconOptions> {
6485
protected options: FaviconOptions;
6586
protected iconOptions: IO[];
@@ -71,10 +92,17 @@ export class Platform<IO extends NamedIconOptions = NamedIconOptions> {
7192

7293
async create(sourceset: SourceImage[]): Promise<FaviconResponse> {
7394
const { output } = this.options;
95+
const images = output.images ? await this.createImages(sourceset) : [];
96+
const files = output.files ? await this.createFiles() : [];
97+
let htmlTags = [];
98+
if (output.html) {
99+
htmlTags = await this.createHtml();
100+
}
74101
return {
75-
images: output.images ? await this.createImages(sourceset) : [],
76-
files: output.files ? await this.createFiles() : [],
77-
html: output.html ? await this.createHtml() : [],
102+
images,
103+
files,
104+
html: htmlTags.map(renderHtmlTag),
105+
htmlTags,
78106
};
79107
}
80108

@@ -90,7 +118,7 @@ export class Platform<IO extends NamedIconOptions = NamedIconOptions> {
90118
return [];
91119
}
92120

93-
async createHtml(): Promise<FaviconHtmlElement[]> {
121+
async createHtml(): Promise<FaviconHtmlTag[]> {
94122
return [];
95123
}
96124

src/platforms/favicons.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FaviconHtmlElement } from "../index";
1+
import { FaviconHtmlTag } from "../index";
22
import { FaviconOptions, NamedIconOptions } from "../config/defaults";
33
import { transparentIcon, transparentIcons } from "../config/icons";
44
import { OptionalMixin, Platform, uniformIconOptions } from "./base";
@@ -19,20 +19,25 @@ export class FaviconsPlatform extends Platform {
1919
);
2020
}
2121

22-
override async createHtml(): Promise<FaviconHtmlElement[]> {
22+
override async createHtml(): Promise<FaviconHtmlTag[]> {
2323
return this.iconOptions.map(({ name, ...options }) => {
24+
const attrs: Record<string, string> = {
25+
rel: "icon",
26+
type: "image/png",
27+
href: this.cacheBusting(this.relative(name)),
28+
};
2429
if (name.endsWith(".ico")) {
25-
// prettier-ignore
26-
return `<link rel="icon" type="image/x-icon" href="${this.cacheBusting(this.relative(name))}">`;
30+
attrs.type = "image/x-icon";
2731
} else if (name.endsWith(".svg")) {
28-
// prettier-ignore
29-
return `<link rel="icon" type="image/svg+xml" href="${this.cacheBusting(this.relative(name))}">`;
32+
attrs.type = "image/svg+xml";
33+
} else {
34+
const { width, height } = options.sizes[0];
35+
attrs.sizes = `${width}x${height}`;
3036
}
31-
32-
const { width, height } = options.sizes[0];
33-
34-
// prettier-ignore
35-
return `<link rel="icon" type="image/png" sizes="${width}x${height}" href="${this.cacheBusting(this.relative(name))}">`;
37+
return {
38+
tag: "link",
39+
attrs,
40+
};
3641
});
3742
}
3843
}

src/platforms/windows.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import xml2js from "xml2js";
2-
import { FaviconFile, FaviconHtmlElement } from "../index";
2+
import { FaviconHtmlTag, FaviconFile } from "../index";
33
import { FaviconOptions, IconSize, NamedIconOptions } from "../config/defaults";
44
import { transparentIcon } from "../config/icons";
55
import { relativeTo } from "../helpers";
@@ -43,16 +43,33 @@ export class WindowsPlatform extends Platform {
4343
return [this.browserConfig()];
4444
}
4545

46-
override async createHtml(): Promise<FaviconHtmlElement[]> {
46+
override async createHtml(): Promise<FaviconHtmlTag[]> {
4747
const tile = "mstile-144x144.png";
4848

49-
// prettier-ignore
5049
return [
51-
`<meta name="msapplication-TileColor" content="${this.options.background}">`,
52-
this.iconOptions.find(iconOption => iconOption.name === tile)
53-
? `<meta name="msapplication-TileImage" content="${this.cacheBusting(this.relative(tile))}">`
54-
: "",
55-
`<meta name="msapplication-config" content="${this.cacheBusting(this.relative(this.manifestFileName()))}">`,
50+
{
51+
tag: "meta",
52+
attrs: {
53+
name: "msapplication-TileColor",
54+
content: this.options.background,
55+
},
56+
},
57+
this.iconOptions.find((iconOption) => iconOption.name === tile)
58+
? {
59+
tag: "meta",
60+
attrs: {
61+
name: "msapplication-TileImage",
62+
content: this.cacheBusting(this.relative(tile)),
63+
},
64+
}
65+
: undefined,
66+
{
67+
tag: "meta",
68+
attrs: {
69+
name: "msapplication-config",
70+
content: this.cacheBusting(this.relative(this.manifestFileName())),
71+
},
72+
},
5673
];
5774
}
5875

src/platforms/yandex.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FaviconFile, FaviconHtmlElement } from "../index";
1+
import { FaviconFile, FaviconHtmlTag } from "../index";
22
import { FaviconOptions, NamedIconOptions } from "../config/defaults";
33
import { transparentIcon } from "../config/icons";
44
import { relativeTo } from "../helpers";
@@ -20,10 +20,15 @@ export class YandexPlatform extends Platform {
2020
return [this.manifest()];
2121
}
2222

23-
override async createHtml(): Promise<FaviconHtmlElement[]> {
24-
// prettier-ignore
23+
override async createHtml(): Promise<FaviconHtmlTag[]> {
2524
return [
26-
`<link rel="yandex-tableau-widget" href="${this.cacheBusting(this.relative(this.manifestFileName()))}">`
25+
{
26+
tag: "link",
27+
attrs: {
28+
rel: "yandex-tableau-widget",
29+
href: this.cacheBusting(this.relative(this.manifestFileName())),
30+
},
31+
},
2732
];
2833
}
2934

0 commit comments

Comments
 (0)