@addfox/utils 0.1.1-beta.2

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 addfox
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,33 @@
1
+ # @addfox/utils
2
+
3
+ [中文](README-zh_CN.md) | English
4
+
5
+ ---
6
+
7
+ Utilities for extension content scripts: **content UI** helpers to define and mount a root element (optionally in iframe or shadow DOM) into a page.
8
+
9
+ - **Entry**: `@addfox/utils` — export `defineContentUI`, `mountContentUI`.
10
+
11
+ ## Content UI
12
+
13
+ ```ts
14
+ import { defineContentUI, mountContentUI } from "@addfox/utils";
15
+
16
+ const spec = defineContentUI({
17
+ tag: "div",
18
+ target: "body",
19
+ attr: { id: "my-root", class: "container" },
20
+ injectMode: "append",
21
+ wrapper: "shadow",
22
+ });
23
+ const root = mountContentUI(spec);
24
+ root.appendChild(myContent);
25
+ ```
26
+
27
+ - **tag**: Element tag name (`"div"`, `"section"`, etc.).
28
+ - **target**: Mount target — CSS selector (for `document.querySelector`) or an `Element`.
29
+ - **attr**: Attributes for the element (`id`, `class`, `style`, `data-*`, etc.).
30
+ - **injectMode**: `"append"` (default) or `"prepend"`.
31
+ - **wrapper**: `"none"` (default), `"shadow"` (attach shadow root), or `"iframe"`.
32
+
33
+ If you need the `browser` API (e.g. `browser.runtime`), install [webextension-polyfill](https://github.com/mozilla/webextension-polyfill) yourself and import from it.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Content UI helpers for extension content scripts: define and mount a root element
3
+ * (optionally inside iframe or shadow DOM) into a page target.
4
+ *
5
+ * Usage:
6
+ * import { defineShadowContentUI } from "@addfox/utils";
7
+ * const mount = defineShadowContentUI({ name: "my-content-ui", target: "body" });
8
+ * const root = mount();
9
+ * root.appendChild(myContent);
10
+ */
11
+ export type ContentUIWrapper = "iframe" | "shadow" | "none";
12
+ export type ContentUIInjectMode = "append" | "prepend";
13
+ export interface DefineContentUIBaseOptions {
14
+ /** Mount target: CSS selector (document.querySelector) or an Element */
15
+ target: string | Element;
16
+ /** Element attributes (id, class, style, data-*, etc.) */
17
+ attr?: Record<string, string>;
18
+ /**
19
+ * How to insert relative to target: append or prepend.
20
+ * When wrapper is used (iframe/shadow), the wrapper element is also inserted into the target according to this mode.
21
+ */
22
+ injectMode?: ContentUIInjectMode;
23
+ }
24
+ export interface DefineContentUIOptions extends DefineContentUIBaseOptions {
25
+ /** Element tag name, e.g. "div", "section" */
26
+ tag: string;
27
+ }
28
+ export interface DefineShadowContentUIOptions extends DefineContentUIBaseOptions {
29
+ /** Custom element name for shadow host, e.g. "my-content-ui". */
30
+ name: string;
31
+ }
32
+ export interface DefineIframeContentUIOptions extends DefineContentUIBaseOptions {
33
+ }
34
+ declare const CONTENT_UI_SPEC: unique symbol;
35
+ export interface ContentUISpecBrand {
36
+ readonly [CONTENT_UI_SPEC]: true;
37
+ }
38
+ export type NativeContentUISpec = DefineContentUIOptions & {
39
+ wrapper: "none";
40
+ };
41
+ export type ShadowContentUISpec = DefineShadowContentUIOptions & {
42
+ wrapper: "shadow";
43
+ };
44
+ export type IframeContentUISpec = DefineIframeContentUIOptions & {
45
+ wrapper: "iframe";
46
+ };
47
+ export type ContentUISpec = (NativeContentUISpec | ShadowContentUISpec | IframeContentUISpec) & ContentUISpecBrand;
48
+ export type ContentUIMount = () => Element | ShadowRoot;
49
+ export declare function defineContentUI(options: DefineContentUIOptions): ContentUIMount;
50
+ export declare function defineShadowContentUI(options: DefineShadowContentUIOptions): ContentUIMount;
51
+ export declare function defineIframeContentUI(options: DefineIframeContentUIOptions): ContentUIMount;
52
+ export {};
@@ -0,0 +1 @@
1
+ export { defineContentUI, defineShadowContentUI, defineIframeContentUI, type ContentUIMount, type ContentUIWrapper, type ContentUIInjectMode, type DefineContentUIBaseOptions, type DefineContentUIOptions, type DefineShadowContentUIOptions, type DefineIframeContentUIOptions, } from "./content-ui";
package/dist/index.js ADDED
@@ -0,0 +1,195 @@
1
+ const CONTENT_CSS_GLOBAL_KEY = "__ADDFOX_CONTENT_CSS_FILES__";
2
+ const CONTENT_CSS_TEXTS_GLOBAL_KEY = "__ADDFOX_CONTENT_CSS_TEXTS__";
3
+ const DEFAULT_SHADOW_COMPONENT_NAME = "addfox-content-ui";
4
+ const contentCssTargets = new WeakSet();
5
+ const CONTENT_UI_SPEC = Symbol.for("@addfox/utils/content-ui-spec");
6
+ function createSpec(options) {
7
+ const spec = {
8
+ ...options,
9
+ injectMode: options.injectMode ?? "append",
10
+ [CONTENT_UI_SPEC]: true
11
+ };
12
+ return spec;
13
+ }
14
+ function defineContentUI(options) {
15
+ const spec = createSpec({
16
+ ...options,
17
+ wrapper: "none"
18
+ });
19
+ return ()=>mountContentUIInternal(spec);
20
+ }
21
+ function defineShadowContentUI(options) {
22
+ const spec = createSpec({
23
+ ...options,
24
+ wrapper: "shadow"
25
+ });
26
+ return ()=>mountContentUIInternal(spec);
27
+ }
28
+ function defineIframeContentUI(options) {
29
+ const spec = createSpec({
30
+ ...options,
31
+ wrapper: "iframe"
32
+ });
33
+ return ()=>mountContentUIInternal(spec);
34
+ }
35
+ function resolveTarget(target) {
36
+ if ("string" == typeof target) return document.querySelector(target);
37
+ return target;
38
+ }
39
+ function applyHostAttrs(el, attr) {
40
+ if (!attr) return;
41
+ for (const [key, value] of Object.entries(attr))if ("style" === key && "string" == typeof value) el.style.cssText = value;
42
+ else el.setAttribute(key, value);
43
+ }
44
+ function injectChild(container, child, mode) {
45
+ if ("prepend" === mode) container.insertBefore(child, container.firstChild);
46
+ else container.appendChild(child);
47
+ }
48
+ function isValidCustomElementName(name) {
49
+ if (!name.includes("-")) return false;
50
+ return /^[a-z][a-z0-9._-]*-[a-z0-9._-]*$/.test(name);
51
+ }
52
+ function normalizeShadowComponentName(name) {
53
+ const normalized = (name ?? DEFAULT_SHADOW_COMPONENT_NAME).trim().toLowerCase();
54
+ if (isValidCustomElementName(normalized)) return normalized;
55
+ return DEFAULT_SHADOW_COMPONENT_NAME;
56
+ }
57
+ function getCustomElementsRegistry() {
58
+ const g = globalThis;
59
+ const registry = g.customElements;
60
+ if (!registry || "object" != typeof registry) return null;
61
+ const maybeGet = registry.get;
62
+ const maybeDefine = registry.define;
63
+ if ("function" != typeof maybeGet || "function" != typeof maybeDefine) return null;
64
+ return registry;
65
+ }
66
+ function ensureCustomElementDefined(tag) {
67
+ const registry = getCustomElementsRegistry();
68
+ if (!registry) return false;
69
+ if (registry.get(tag)) return true;
70
+ class AddfoxShadowElement extends HTMLElement {
71
+ }
72
+ registry.define(tag, AddfoxShadowElement);
73
+ return true;
74
+ }
75
+ function getRuntimeUrl(path) {
76
+ const value = path.replace(/\\/g, "/");
77
+ const g = globalThis;
78
+ const chromeUrl = g.chrome?.runtime?.getURL;
79
+ if ("function" == typeof chromeUrl) return chromeUrl(value);
80
+ const browserUrl = g.browser?.runtime?.getURL;
81
+ if ("function" == typeof browserUrl) return browserUrl(value);
82
+ return value;
83
+ }
84
+ function readContentCssFilesFromGlobal() {
85
+ const g = globalThis;
86
+ const list = g[CONTENT_CSS_GLOBAL_KEY];
87
+ if (!Array.isArray(list)) return [];
88
+ return list.filter((item)=>"string" == typeof item && item.length > 0);
89
+ }
90
+ function readContentCssTextsFromGlobal() {
91
+ const g = globalThis;
92
+ const list = g[CONTENT_CSS_TEXTS_GLOBAL_KEY];
93
+ if (!Array.isArray(list)) return [];
94
+ return list.filter((item)=>"string" == typeof item && item.length > 0);
95
+ }
96
+ function injectCssIntoNode(targetNode, createStyleEl) {
97
+ if (contentCssTargets.has(targetNode)) return;
98
+ contentCssTargets.add(targetNode);
99
+ const cssTexts = readContentCssTextsFromGlobal();
100
+ if (cssTexts.length > 0) {
101
+ for (const cssText of cssTexts){
102
+ const style = createStyleEl();
103
+ style.textContent = cssText;
104
+ }
105
+ return;
106
+ }
107
+ const files = readContentCssFilesFromGlobal();
108
+ if (0 === files.length) return;
109
+ for (const rel of files){
110
+ const url = getRuntimeUrl(rel);
111
+ fetch(url).then((res)=>res.ok ? res.text() : "").then((cssText)=>{
112
+ if (!cssText) return;
113
+ const style = createStyleEl();
114
+ style.textContent = cssText;
115
+ }).catch(()=>{});
116
+ }
117
+ }
118
+ function injectCssIntoShadowHead(shadow, head) {
119
+ injectCssIntoNode(shadow, ()=>{
120
+ const style = document.createElement("style");
121
+ head.appendChild(style);
122
+ return style;
123
+ });
124
+ }
125
+ function ensureIframeHead(doc) {
126
+ if (doc.head) return doc.head;
127
+ const head = doc.createElement("head");
128
+ const html = doc.documentElement ?? doc.createElement("html");
129
+ if (!doc.documentElement) doc.appendChild(html);
130
+ html.insertBefore(head, html.firstChild);
131
+ return head;
132
+ }
133
+ function injectCssIntoIframeHead(doc) {
134
+ const head = ensureIframeHead(doc);
135
+ injectCssIntoNode(doc, ()=>{
136
+ const style = doc.createElement("style");
137
+ head.appendChild(style);
138
+ return style;
139
+ });
140
+ }
141
+ function mountNone(spec, container) {
142
+ const el = document.createElement(spec.tag);
143
+ applyHostAttrs(el, spec.attr);
144
+ injectChild(container, el, spec.injectMode ?? "append");
145
+ return el;
146
+ }
147
+ function mountShadow(spec, container) {
148
+ const hostTag = normalizeShadowComponentName(spec.name);
149
+ try {
150
+ ensureCustomElementDefined(hostTag);
151
+ } catch {}
152
+ const host = document.createElement(hostTag);
153
+ applyHostAttrs(host, spec.attr);
154
+ const shadow = host.attachShadow({
155
+ mode: "open"
156
+ });
157
+ const html = document.createElement("html");
158
+ const head = document.createElement("head");
159
+ const body = document.createElement("body");
160
+ const mountNode = document.createElement("div");
161
+ mountNode.setAttribute("data-addfox-shadow-root", "true");
162
+ html.appendChild(head);
163
+ body.appendChild(mountNode);
164
+ html.appendChild(body);
165
+ shadow.appendChild(html);
166
+ injectCssIntoShadowHead(shadow, head);
167
+ injectChild(container, host, spec.injectMode ?? "append");
168
+ return mountNode;
169
+ }
170
+ function mountIframe(spec, container) {
171
+ const iframe = document.createElement("iframe");
172
+ applyHostAttrs(iframe, spec.attr);
173
+ injectChild(container, iframe, spec.injectMode ?? "append");
174
+ const doc = iframe.contentDocument;
175
+ if (!doc) throw new Error("content-ui: iframe contentDocument not available");
176
+ injectCssIntoIframeHead(doc);
177
+ const root = document.createElement("div");
178
+ doc.body.appendChild(root);
179
+ return root;
180
+ }
181
+ function mountContentUIInternal(spec) {
182
+ const container = resolveTarget(spec.target);
183
+ if (!container) throw new Error(`content-ui: target not found (${"string" == typeof spec.target ? spec.target : "Element"})`);
184
+ switch(spec.wrapper){
185
+ case "shadow":
186
+ return mountShadow(spec, container);
187
+ case "iframe":
188
+ return mountIframe(spec, container);
189
+ default:
190
+ return mountNone(spec, container);
191
+ }
192
+ }
193
+ export { defineContentUI, defineIframeContentUI, defineShadowContentUI };
194
+
195
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/content-ui.ts"],"sourcesContent":["/**\r\n * Content UI helpers for extension content scripts: define and mount a root element\r\n * (optionally inside iframe or shadow DOM) into a page target.\r\n *\r\n * Usage:\r\n * import { defineShadowContentUI } from \"@addfox/utils\";\r\n * const mount = defineShadowContentUI({ name: \"my-content-ui\", target: \"body\" });\r\n * const root = mount();\r\n * root.appendChild(myContent);\r\n */\r\n\r\nexport type ContentUIWrapper = \"iframe\" | \"shadow\" | \"none\";\r\nexport type ContentUIInjectMode = \"append\" | \"prepend\";\r\nconst CONTENT_CSS_GLOBAL_KEY = \"__ADDFOX_CONTENT_CSS_FILES__\";\r\nconst CONTENT_CSS_TEXTS_GLOBAL_KEY = \"__ADDFOX_CONTENT_CSS_TEXTS__\";\r\nconst DEFAULT_SHADOW_COMPONENT_NAME = \"addfox-content-ui\";\r\nconst contentCssTargets = new WeakSet<Node>();\r\n\r\nexport interface DefineContentUIBaseOptions {\r\n /** Mount target: CSS selector (document.querySelector) or an Element */\r\n target: string | Element;\r\n /** Element attributes (id, class, style, data-*, etc.) */\r\n attr?: Record<string, string>;\r\n /**\r\n * How to insert relative to target: append or prepend.\r\n * When wrapper is used (iframe/shadow), the wrapper element is also inserted into the target according to this mode.\r\n */\r\n injectMode?: ContentUIInjectMode;\r\n}\r\n\r\nexport interface DefineContentUIOptions extends DefineContentUIBaseOptions {\r\n /** Element tag name, e.g. \"div\", \"section\" */\r\n tag: string;\r\n}\r\n\r\nexport interface DefineShadowContentUIOptions extends DefineContentUIBaseOptions {\r\n /** Custom element name for shadow host, e.g. \"my-content-ui\". */\r\n name: string;\r\n}\r\n\r\nexport interface DefineIframeContentUIOptions extends DefineContentUIBaseOptions {\r\n}\r\n\r\nconst CONTENT_UI_SPEC = Symbol.for(\"@addfox/utils/content-ui-spec\");\r\n\r\nexport interface ContentUISpecBrand {\r\n readonly [CONTENT_UI_SPEC]: true;\r\n}\r\n\r\nexport type NativeContentUISpec = DefineContentUIOptions & {\r\n wrapper: \"none\";\r\n};\r\n\r\nexport type ShadowContentUISpec = DefineShadowContentUIOptions & {\r\n wrapper: \"shadow\";\r\n};\r\n\r\nexport type IframeContentUISpec = DefineIframeContentUIOptions & {\r\n wrapper: \"iframe\";\r\n};\r\n\r\nexport type ContentUISpec = (\r\n | NativeContentUISpec\r\n | ShadowContentUISpec\r\n | IframeContentUISpec\r\n) & ContentUISpecBrand;\r\n\r\nexport type ContentUIMount = () => Element | ShadowRoot;\r\n\r\n/**\r\n * Defines a content UI spec. Use the return value with mountContentUI() when you want to mount.\r\n */\r\nfunction createSpec<T extends NativeContentUISpec | ShadowContentUISpec | IframeContentUISpec>(\r\n options: T\r\n): ContentUISpec {\r\n const spec = {\r\n ...options,\r\n injectMode: options.injectMode ?? \"append\" as ContentUIInjectMode,\r\n [CONTENT_UI_SPEC]: true as const,\r\n };\r\n return spec as ContentUISpec;\r\n}\r\n\r\nexport function defineContentUI(\r\n options: DefineContentUIOptions\r\n): ContentUIMount {\r\n const spec = createSpec({\r\n ...options,\r\n wrapper: \"none\",\r\n });\r\n return () => mountContentUIInternal(spec);\r\n}\r\n\r\nexport function defineShadowContentUI(\r\n options: DefineShadowContentUIOptions\r\n): ContentUIMount {\r\n const spec = createSpec({\r\n ...options,\r\n wrapper: \"shadow\",\r\n });\r\n return () => mountContentUIInternal(spec);\r\n}\r\n\r\nexport function defineIframeContentUI(\r\n options: DefineIframeContentUIOptions\r\n): ContentUIMount {\r\n const spec = createSpec({\r\n ...options,\r\n wrapper: \"iframe\",\r\n });\r\n return () => mountContentUIInternal(spec);\r\n}\r\n\r\nfunction resolveTarget(target: string | Element): Element | null {\r\n if (typeof target === \"string\") {\r\n return document.querySelector(target);\r\n }\r\n return target;\r\n}\r\n\r\n/** Apply attrs to host element (the node inserted into target) in all modes. */\r\nfunction applyHostAttrs(\r\n el: Element,\r\n attr: Record<string, string> | undefined\r\n): void {\r\n if (!attr) return;\r\n for (const [key, value] of Object.entries(attr)) {\r\n if (key === \"style\" && typeof value === \"string\") {\r\n (el as HTMLElement).style.cssText = value;\r\n } else {\r\n el.setAttribute(key, value);\r\n }\r\n }\r\n}\r\n\r\nfunction injectChild(\r\n container: Element,\r\n child: Element,\r\n mode: ContentUIInjectMode\r\n): void {\r\n if (mode === \"prepend\") {\r\n container.insertBefore(child, container.firstChild);\r\n } else {\r\n container.appendChild(child);\r\n }\r\n}\r\n\r\nfunction isValidCustomElementName(name: string): boolean {\r\n if (!name.includes(\"-\")) return false;\r\n return /^[a-z][a-z0-9._-]*-[a-z0-9._-]*$/.test(name);\r\n}\r\n\r\nfunction normalizeShadowComponentName(name: string | undefined): string {\r\n const normalized = (name ?? DEFAULT_SHADOW_COMPONENT_NAME).trim().toLowerCase();\r\n if (isValidCustomElementName(normalized)) return normalized;\r\n return DEFAULT_SHADOW_COMPONENT_NAME;\r\n}\r\n\r\nfunction getCustomElementsRegistry():\r\n | { get: (name: string) => unknown; define: (name: string, ctor: CustomElementConstructor) => void }\r\n | null {\r\n const g = globalThis as { customElements?: unknown };\r\n const registry = g.customElements;\r\n if (!registry || typeof registry !== \"object\") return null;\r\n const maybeGet = (registry as { get?: unknown }).get;\r\n const maybeDefine = (registry as { define?: unknown }).define;\r\n if (typeof maybeGet !== \"function\" || typeof maybeDefine !== \"function\") return null;\r\n return registry as {\r\n get: (name: string) => unknown;\r\n define: (name: string, ctor: CustomElementConstructor) => void;\r\n };\r\n}\r\n\r\nfunction ensureCustomElementDefined(tag: string): boolean {\r\n const registry = getCustomElementsRegistry();\r\n if (!registry) return false;\r\n if (registry.get(tag)) return true;\r\n class AddfoxShadowElement extends HTMLElement {}\r\n registry.define(tag, AddfoxShadowElement);\r\n return true;\r\n}\r\n\r\nfunction getRuntimeUrl(path: string): string {\r\n const value = path.replace(/\\\\/g, \"/\");\r\n const g = globalThis as {\r\n chrome?: { runtime?: { getURL?: (p: string) => string } };\r\n browser?: { runtime?: { getURL?: (p: string) => string } };\r\n };\r\n const chromeUrl = g.chrome?.runtime?.getURL;\r\n if (typeof chromeUrl === \"function\") return chromeUrl(value);\r\n const browserUrl = g.browser?.runtime?.getURL;\r\n if (typeof browserUrl === \"function\") return browserUrl(value);\r\n return value;\r\n}\r\n\r\nfunction readContentCssFilesFromGlobal(): string[] {\r\n const g = globalThis as { [CONTENT_CSS_GLOBAL_KEY]?: unknown };\r\n const list = g[CONTENT_CSS_GLOBAL_KEY];\r\n if (!Array.isArray(list)) return [];\r\n return list.filter((item): item is string => typeof item === \"string\" && item.length > 0);\r\n}\r\n\r\nfunction readContentCssTextsFromGlobal(): string[] {\r\n const g = globalThis as { [CONTENT_CSS_TEXTS_GLOBAL_KEY]?: unknown };\r\n const list = g[CONTENT_CSS_TEXTS_GLOBAL_KEY];\r\n if (!Array.isArray(list)) return [];\r\n return list.filter((item): item is string => typeof item === \"string\" && item.length > 0);\r\n}\r\n\r\nfunction injectCssIntoNode(\r\n targetNode: Node,\r\n createStyleEl: () => HTMLStyleElement\r\n): void {\r\n if (contentCssTargets.has(targetNode)) return;\r\n contentCssTargets.add(targetNode);\r\n const cssTexts = readContentCssTextsFromGlobal();\r\n if (cssTexts.length > 0) {\r\n for (const cssText of cssTexts) {\r\n const style = createStyleEl();\r\n style.textContent = cssText;\r\n }\r\n return;\r\n }\r\n\r\n const files = readContentCssFilesFromGlobal();\r\n if (files.length === 0) return;\r\n\r\n for (const rel of files) {\r\n const url = getRuntimeUrl(rel);\r\n void fetch(url)\r\n .then((res) => (res.ok ? res.text() : \"\"))\r\n .then((cssText) => {\r\n if (!cssText) return;\r\n const style = createStyleEl();\r\n style.textContent = cssText;\r\n })\r\n .catch(() => {\r\n // ignore css fetch errors\r\n });\r\n }\r\n}\r\n\r\nfunction injectCssIntoShadowHead(\r\n shadow: ShadowRoot,\r\n head: HTMLElement\r\n): void {\r\n injectCssIntoNode(shadow, () => {\r\n const style = document.createElement(\"style\");\r\n head.appendChild(style);\r\n return style;\r\n });\r\n}\r\n\r\nfunction ensureIframeHead(doc: Document): HTMLHeadElement {\r\n if (doc.head) return doc.head;\r\n const head = doc.createElement(\"head\");\r\n const html = doc.documentElement ?? doc.createElement(\"html\");\r\n if (!doc.documentElement) doc.appendChild(html);\r\n html.insertBefore(head, html.firstChild);\r\n return head;\r\n}\r\n\r\nfunction injectCssIntoIframeHead(doc: Document): void {\r\n const head = ensureIframeHead(doc);\r\n injectCssIntoNode(doc, () => {\r\n const style = doc.createElement(\"style\");\r\n head.appendChild(style);\r\n return style;\r\n });\r\n}\r\n\r\nfunction mountNone(spec: NativeContentUISpec, container: Element): Element {\r\n const el = document.createElement(spec.tag);\r\n applyHostAttrs(el, spec.attr);\r\n injectChild(container, el, spec.injectMode ?? \"append\");\r\n return el;\r\n}\r\n\r\nfunction mountShadow(spec: ShadowContentUISpec, container: Element): HTMLElement {\r\n const hostTag = normalizeShadowComponentName(spec.name);\r\n // Always create host with custom tag name.\r\n // Try to define it when registry is available, but never fallback to div.\r\n try {\r\n ensureCustomElementDefined(hostTag);\r\n } catch {\r\n // ignore define errors; host tag still keeps the custom element name\r\n }\r\n const host = document.createElement(hostTag);\r\n applyHostAttrs(host, spec.attr);\r\n const shadow = host.attachShadow({ mode: \"open\" });\r\n const html = document.createElement(\"html\");\r\n const head = document.createElement(\"head\");\r\n const body = document.createElement(\"body\");\r\n const mountNode = document.createElement(\"div\");\r\n mountNode.setAttribute(\"data-addfox-shadow-root\", \"true\");\r\n html.appendChild(head);\r\n body.appendChild(mountNode);\r\n html.appendChild(body);\r\n shadow.appendChild(html);\r\n injectCssIntoShadowHead(shadow, head);\r\n // wrapper (host) is inserted into target according to injectMode\r\n injectChild(container, host, spec.injectMode ?? \"append\");\r\n return mountNode;\r\n}\r\n\r\nfunction mountIframe(spec: IframeContentUISpec, container: Element): HTMLElement {\r\n const iframe = document.createElement(\"iframe\");\r\n applyHostAttrs(iframe, spec.attr);\r\n // wrapper (iframe) is inserted into target according to injectMode\r\n injectChild(container, iframe, spec.injectMode ?? \"append\");\r\n const doc = iframe.contentDocument;\r\n if (!doc) throw new Error(\"content-ui: iframe contentDocument not available\");\r\n injectCssIntoIframeHead(doc);\r\n const root = document.createElement(\"div\");\r\n doc.body.appendChild(root);\r\n return root;\r\n}\r\n\r\nfunction mountContentUIInternal(spec: ContentUISpec): Element | ShadowRoot {\r\n const container = resolveTarget(spec.target);\r\n if (!container) {\r\n throw new Error(\r\n `content-ui: target not found (${\r\n typeof spec.target === \"string\" ? spec.target : \"Element\"\r\n })`\r\n );\r\n }\r\n switch (spec.wrapper) {\r\n case \"shadow\":\r\n return mountShadow(spec as ShadowContentUISpec, container);\r\n case \"iframe\":\r\n return mountIframe(spec as IframeContentUISpec, container);\r\n default:\r\n return mountNone(spec as NativeContentUISpec, container);\r\n }\r\n}\r\n"],"names":["CONTENT_CSS_GLOBAL_KEY","CONTENT_CSS_TEXTS_GLOBAL_KEY","DEFAULT_SHADOW_COMPONENT_NAME","contentCssTargets","WeakSet","CONTENT_UI_SPEC","Symbol","createSpec","options","spec","defineContentUI","mountContentUIInternal","defineShadowContentUI","defineIframeContentUI","resolveTarget","target","document","applyHostAttrs","el","attr","key","value","Object","injectChild","container","child","mode","isValidCustomElementName","name","normalizeShadowComponentName","normalized","getCustomElementsRegistry","g","globalThis","registry","maybeGet","maybeDefine","ensureCustomElementDefined","tag","AddfoxShadowElement","HTMLElement","getRuntimeUrl","path","chromeUrl","browserUrl","readContentCssFilesFromGlobal","list","Array","item","readContentCssTextsFromGlobal","injectCssIntoNode","targetNode","createStyleEl","cssTexts","cssText","style","files","rel","url","fetch","res","injectCssIntoShadowHead","shadow","head","ensureIframeHead","doc","html","injectCssIntoIframeHead","mountNone","mountShadow","hostTag","host","body","mountNode","mountIframe","iframe","Error","root"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@addfox/utils",
3
+ "version": "0.1.1-beta.2",
4
+ "description": "Extension utils: content UI (defineContentUI / mountContentUI) for content scripts",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ }
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "devDependencies": {
16
+ "@rslib/core": "^0.20.0",
17
+ "@rstest/core": "^0.9.2",
18
+ "@rstest/coverage-istanbul": "^0.3.0",
19
+ "@types/node": "^20.0.0",
20
+ "typescript": "^5.0.0"
21
+ },
22
+ "scripts": {
23
+ "build": "rslib build",
24
+ "dev": "rslib build --watch",
25
+ "test": "rstest run",
26
+ "test:coverage": "rstest run --coverage"
27
+ }
28
+ }