/
DocumentFragment.class.ts
230 lines (215 loc) · 9.76 KB
/
DocumentFragment.class.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import * as fs from 'fs'
import * as path from 'path'
import * as util from 'util'
import * as jsdom from 'jsdom'
import * as xjs from 'extrajs'
import {Content} from '../ambient'
import xjs_ParentNode from '../iface/ParentNode.iface'
import xjs_Node, { NodeType } from './Node.class'
import xjs_Element from './Element.class'
import xjs_HTMLTemplateElement_import from './HTMLTemplateElement.class'
/**
* Wrapper for a DocumentFragment.
* @see https://www.w3.org/TR/dom/#documentfragment
*/
export default class xjs_DocumentFragment extends xjs_Node implements xjs_ParentNode {
/**
* Concatenate multiple contents into text.
*
* ```js
* xjs.DocumentFragment.concat(
* new xjs.Element(document.createElement('strong')).append(`hello `),
* new xjs.Element(document.createElement('em' )).append(`world`),
* new xjs.Element(document.createElement('mark' )).append(`!`)
* ) // '<strong>hello </strong><em>world</em><mark>!</mark>'
* ```
* @param contents the contents to concatenate
* @returns the resulting output of concatenation
*/
static concat(...contents: Content[]): string {
return new xjs_DocumentFragment(jsdom.JSDOM.fragment('')).append(...contents).innerHTML()
}
/**
* Read an HTML string and return a document fragment with its contents.
*
* The DocumentFragment object will be wrapped in an `xjs.DocumentFragment` object.
* To access the actual fragment, call {@link xjs_DocumentFragment#node}.
* @param str a string of markup
* @returns the fragment, wrapped
*/
static fromString(str: string): xjs_DocumentFragment {
return new xjs_DocumentFragment(jsdom.JSDOM.fragment(str))
}
/**
* Read an HTML file and return a document fragment with its contents.
*
* The DocumentFragment object will be wrapped in an `xjs.DocumentFragment` object.
* To access the actual fragment, call {@link xjs_DocumentFragment#node}.
* @param filepath the path to the file
* @returns the fragment, wrapped
*/
static async fromFile(filepath: string): Promise<xjs_DocumentFragment> {
return xjs_DocumentFragment.fromString(await util.promisify(fs.readFile)(filepath, 'utf8'))
}
/**
* Synchronous version of {@link xjs_DocumentFragment.fromFile}.
* @param filepath the path to the file
* @returns the fragment, wrapped
*/
static fromFileSync(filepath: string): xjs_DocumentFragment {
return xjs_DocumentFragment.fromString(fs.readFileSync(filepath, 'utf8'))
}
/**
* Construct a new xjs_DocumentFragment object.
* @param node the node to wrap
*/
constructor(node: DocumentFragment) {
super(node)
}
/**
* This wrapper’s node.
*/
get node(): DocumentFragment { return super.node as DocumentFragment }
/** @implements xjs_ParentNode */
prepend(...contents: Content[]): this {
this.node.prepend(...contents.map((c) =>
(c instanceof xjs_Node) ? c.node :
(c === null) ? '' : c
))
return this
}
/** @implements xjs_ParentNode */
append(...contents: Content[]): this {
this.node.append(...contents.map((c) =>
(c instanceof xjs_Node) ? c.node :
(c === null) ? '' : c
))
return this
}
/** @implements xjs_ParentNode */
querySelector(selector: string): xjs_Element|null {
let el: Element|null = this.node.querySelector(selector)
return (el === null) ? null : new xjs_Element(el)
}
/** @implements xjs_ParentNode */
querySelectorAll(selector: string): xjs_Element[] {
return [...this.node.querySelectorAll(selector)].map((el) => new xjs_Element(el))
}
/**
* Get the "innerHTML" of this document fragment.
* @returns a concatenation of all the `outerHTML` and/or data of the fragment’s node children
*/
innerHTML(): string {
return [...this.node.childNodes].map((node) =>
xjs.Object.switch<string>(`${node.nodeType}`, {
[NodeType.ELEMENT_NODE] : (el : Element ) => el.outerHTML,
[NodeType.TEXT_NODE] : (text: Text ) => text.data,
[NodeType.COMMENT_NODE] : (comm: Comment ) => `<!--${comm.data}-->`,
[NodeType.DOCUMENT_FRAGMENT_NODE]: (frag: DocumentFragment) => new xjs_DocumentFragment(frag).innerHTML(),
default: () => '',
})(node)
).join('')
}
/**
* Replace all `link[rel~="import"][data-import]` elements with contents from their documents.
*
* This method finds all `link[rel~="import"][data-import]`s in this document fragment,
* and then replaces those links with another `DocumentFragment` holding some contents.
* These contents depend on the value set for `data-import`:
*
* - if `[data-import="document"]`, then the replaced contents will be the contents of the link’s imported document itself
* - if `[data-import="template"]`, then the replaced contents will be the contents of the first `template` descendant in the link’s imported document
* - if the `[data-import]` attribute value is neither `"document"` nor `"template"`, or if it is absent, then the `link` element is completely ignored and left as-is
*
* Note: If {@link https://developer.mozilla.org/en-US/docs/Web/Web_Components/HTML_Imports|HTMLLinkElement#import}
* is supported (by the browser or jsdom), then when `[data-import="document"]` is set,
* the appended contents will instead be a `Document` object, as defined by
* {@link https://www.w3.org/TR/html-imports/|HTML Imports}, rather than a `DocumentFragment` object.
*
* Note: `DocumentFragment#querySelectorAll` does *not* traverse inside `<template>` elements,
* so any `<link>` elements inside `<template>` elements will be left untouched.
* To modify those, you will need to call this method on that `<template>`’s contents (another `DocumentFragment`).
*
* In the example below,
* The `link[rel="import"]` in this fragment has `[data-import="template"]`, and so is replaced with
* the contents of `template#sect-template` in `x-linked-doc.tpl.html`---namely,
* a `DocumentFragment` containing only the `section` element.
* However, if the link had had `[data-import="document"]`, then the replaced content would consist of
* a `DocumentFragment` containing the entirety of `x-linked-doc.tpl.html`,
* including both the `h1` along with the `template#sect-template`.
*
* ```js
* // x-linked-doc.tpl.html:
* <h1>top-level hed</h1>
* <template id="sect-template">
* <section>
* <h2>section hed</h2>
* <p>a graf</p>
* </section>
* </template>
*
* // main.js:
* // assume `this` is an `xjs.DocumentFragment` instance.
* this.innerHTML() === `
* <ol>
* <template id="list-template">
* <li>
* <link rel="import" data-import="template" href="./x-linked-doc.tpl.html"/>
* </li>
* </template>
* </ol>
* `
*
* // This call will do nothing, as there are no direct `<link>` descendants:
* // `.querySelectorAll` does not traverse inside `<template>`s.
* this.importLinks(__dirname)
*
* // This call will work as intended.
* let innerfrag = new xjs.DocumentFragment(this.node.querySelector('template').content)
* innerfrag.importLinks(__dirname)
* ```
*
* @param dirpath the absolute path to the directory of the template file containing the `link` element
* @returns `this`
*/
importLinks(dirpath: string): this {
const xjs_HTMLTemplateElement: typeof xjs_HTMLTemplateElement_import = require('./HTMLTemplateElement.class').default
if (!('import' in jsdom.JSDOM.fragment('<link rel="import" href="https://example.com/"/>').querySelector('link') !)) {
console.warn('`HTMLLinkElement#import` is not yet supported. Replacing `<link>`s with their imported contents…')
this.node.querySelectorAll('link[rel~="import"][data-import]').forEach((link) => {
let imported: DocumentFragment|null = xjs.Object.switch<DocumentFragment|null>(link.getAttribute('data-import') !, {
'document': (lnk: HTMLLinkElement) => xjs_DocumentFragment .fromFileSync(path.resolve(dirpath, lnk.href)).node,
'template': (lnk: HTMLLinkElement) => xjs_HTMLTemplateElement.fromFileSync(path.resolve(dirpath, lnk.href)).content(),
default: () => null,
})(link)
if (imported) {
link.after(imported)
link.remove() // link.href = path.resolve('https://example.com/index.html', link.href) // TODO set the href relative to the current window.location.href
}
})
}
return this
}
/**
* Asynchronous version of {@link xjs_DocumentFragment.importLinks}.
* @param dirpath the absolute path to the directory of the template file containing the `link` element
*/
async importLinksAsync(dirpath: string): Promise<this> {
const xjs_HTMLTemplateElement: Promise<typeof xjs_HTMLTemplateElement_import> = import('./HTMLTemplateElement.class').then((m) => m.default)
if (!('import' in jsdom.JSDOM.fragment('<link rel="import" href="https://example.com/"/>').querySelector('link') !)) {
console.warn('`HTMLLinkElement#import` is not yet supported. Replacing `<link>`s with their imported contents…')
await Promise.all([...this.node.querySelectorAll('link[rel~="import"][data-import]')].map(async (link) => {
let imported: DocumentFragment|null = await xjs.Object.switch<Promise<DocumentFragment>|null>(link.getAttribute('data-import') !, {
'document': async (lnk: HTMLLinkElement) => (await xjs_DocumentFragment .fromFile(path.resolve(dirpath, lnk.href))).node,
'template': async (lnk: HTMLLinkElement) => (await (await xjs_HTMLTemplateElement).fromFile(path.resolve(dirpath, lnk.href))).content(),
default: () => null,
})(link)
if (imported) {
link.after(imported)
link.remove() // link.href = path.resolve('https://example.com/index.html', link.href) // TODO set the href relative to the current window.location.href
}
}))
}
return this
}
}