doohtml 0.98.9-beta.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/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # DooHTML
2
+ DooHTML is a lightweight rendering framework designed for efficient DOM manipulation and templating. Authored by Henrik Javen.
3
+
4
+ ## Features
5
+ - **Template-based Rendering**: Use HTML templates to define reusable components.
6
+ - **Dynamic Data Binding**: Bind data to templates with `{{property}}` placeholders.
7
+ - **Lightweight and Fast**: Optimized for performance with minimal overhead.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install doohtml
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ Define a `<template>` with a `data-bind` row and `{{...}}` placeholders, and a target element with `data-template` and `data-key`. Then call `createTemplate` and `render` (or `add` to append).
18
+
19
+ **HTML:**
20
+
21
+ ```html
22
+ <template id="item-tpl">
23
+ <div>
24
+ <div data-bind data-key="id">
25
+ <span class="name">{{name}}</span>
26
+ <span class="score">{{score}}</span>
27
+ </div>
28
+ </div>
29
+ </template>
30
+ <div data-template="item-tpl" data-key="id"></div>
31
+ ```
32
+
33
+ **JavaScript:**
34
+
35
+ ```javascript
36
+ import { createTemplate, render, add } from 'doohtml';
37
+
38
+ const data = [
39
+ { id: 1, name: 'Alice', score: 10 },
40
+ { id: 2, name: 'Bob', score: 20 }
41
+ ];
42
+
43
+ // Create from template and replace content with data
44
+ const target = await createTemplate('item-tpl', data);
45
+ // target is the placeholder node; it now shows the rendered rows.
46
+
47
+ // Replace all rows
48
+ render(target, data);
49
+
50
+ // Or append more rows (e.g. for infinite scroll)
51
+ add(target, [{ id: 3, name: 'Carol', score: 30 }]);
52
+ ```
53
+
54
+ Use `render(target, data)` to replace the list; use `add(target, data)` to append rows without clearing.
55
+
56
+ ## API overview
57
+
58
+ - **createTemplate**(id, data?, src?) – Creates a template instance from an element id (or selector), optionally renders initial data, returns the target node.
59
+ - **render**(target, data, start?) – Replaces target content with rows from `data`.
60
+ - **add**(target, dataSet, start?) – Appends rows from `dataSet` to the target.
61
+ - **renderWithProvider**(target, dataList, start?, length?, dataProvider?) – Replace content using a data list and optional provider (default: `(i, list) => list[i]`).
62
+ - **addWithProvider**(target, dataList, start?, length?, dataProvider?) – Append rows using a data list and optional provider.
63
+ - **prefetchTemplate**(src) – Preload a template from a URL.
64
+
65
+ ---
66
+
67
+ ## Development
68
+
69
+ ### Prerequisites
70
+
71
+ - **Node.js** (v12 or higher)
72
+ - **npm**
73
+
74
+ ### Build
75
+
76
+ From the project directory:
77
+
78
+ ```bash
79
+ npm install
80
+ npm run build
81
+ ```
82
+
83
+ This runs `build:copy` and `build:min`:
84
+
85
+ - **build:copy** – Copies `src/doohtml.js` to `dist/doohtml.js` and `dist/doohtml.mjs`.
86
+ - **build:min** – Minifies `src/doohtml.js` with Terser (preserving quoted properties) to `dist/doohtml.min.mjs`.
87
+
88
+ So `dist/` contains: `doohtml.js`, `doohtml.mjs`, and `doohtml.min.mjs`.
89
+
90
+ ### Project structure
91
+
92
+ ```
93
+ doohtml/
94
+ ├── src/
95
+ │ └── doohtml.js # Main source
96
+ ├── dist/
97
+ │ ├── doohtml.js # ESM copy (require)
98
+ │ ├── doohtml.mjs # ESM copy (import)
99
+ │ └── doohtml.min.mjs # Minified ESM
100
+ ├── package.json
101
+ └── README.md
102
+ ```
103
+
104
+ ### Other scripts
105
+
106
+ - **preview** – `npm run build && npm pack` (creates a tarball for local testing).
107
+ - **prepublishOnly** – Runs `npm run build` before publish.
108
+
109
+ ---
110
+
111
+ ## License
112
+
113
+ This project is licensed under the MIT License. See the `LICENSE` file for details.
@@ -0,0 +1,440 @@
1
+ const Config = {
2
+ NAME:'DooHTML',
3
+ DATA_BIND:'data-bind',
4
+ DATA_TEMPLATE:'data-template',
5
+ MATCH:{ANY:-1,STARTS_WITH:0,EXACT:1},
6
+ DELIMITER:{'BEG':'{{','END':'}}'},
7
+ DATA_KEY:'data-key',
8
+ KEY:'key'
9
+ }
10
+
11
+ const {cloneNode, appendChild} = globalThis.Node.prototype
12
+ const cloneDeep = n => cloneNode.call(n, true)
13
+ const appendRow = function(child) {
14
+ return appendChild.call(this, child);
15
+ };
16
+
17
+ const version = 'v0.98.9-beta.0'
18
+
19
+ const getItemValue = (item, prop) => {
20
+ if (!prop.includes('.')) {
21
+ return item[prop] ? item[prop] : ''
22
+ }
23
+
24
+ return prop.split('.').reduce((acc, key) => {
25
+ return acc && acc[key] !== undefined ? acc[key] : ''
26
+ }, item)
27
+ }
28
+
29
+ const getNode = (node, arr) => arr.reduce((currentNode, index) => currentNode?.childNodes[index] || null, node)
30
+
31
+
32
+ const isTable = (node) => {
33
+ if (!node || !node.tagName) return false
34
+ return ['TABLE','TBODY','THEAD','TFOOT','TR','TH'].includes(node.tagName)
35
+ }
36
+
37
+ const setNodeValues = (node, dataItem, dataSlots) => {
38
+ const len = dataSlots.length
39
+ for (let x = 0; x < len; x++) {
40
+ const curNode = getNode(node, dataSlots[x][1], 0)
41
+ if (curNode) {
42
+ if (dataSlots[x][2] === 'textContent') {
43
+ curNode.nodeValue = dataItem[dataSlots[x][0]]
44
+ } else {
45
+ curNode.setAttribute(dataSlots[x][2], dataItem[dataSlots[x][0]])
46
+ }
47
+ } else {
48
+ globalThis.console.log('Field:' + dataSlots[x][0] + ' does not exist')
49
+ }
50
+ }
51
+ }
52
+
53
+ const render = (target, data, start = 0) => {
54
+ if (data.length === 0) {
55
+ target.textContent = ''
56
+ return
57
+ }
58
+ renderHTML(target, data, start)
59
+ }
60
+
61
+ const renderHTML = (target, data, start = 0, end=null) => {
62
+ let dataLen = data.length
63
+
64
+ let stop = end ? start + dataLen : dataLen - start
65
+ if (stop > dataLen) { stop = dataLen }
66
+
67
+ // Clear existing content
68
+ target.textContent = ''
69
+ const insertRow = appendRow.bind(target)
70
+ const parentElement = target.parentElement
71
+ const key = target[Config.KEY]
72
+ const childNodes = target.childNodes
73
+ const isTableSection = target.tagName === 'TBODY' || target.tagName === 'THEAD' || target.tagName === 'TFOOT'
74
+ if (!isTableSection) {
75
+ target.remove()
76
+ }
77
+ let fragment = globalThis.document.createDocumentFragment()
78
+ if (childNodes.length > 0) {
79
+ for (let i = start; i < stop; ++i) {
80
+ setNodeValues(target.processNode, data[i], target.dataSlots)
81
+ let cloned = cloneDeep(target.processNode)
82
+ cloned[Config.KEY] = getItemValue(data[i],key)
83
+ fragment.appendChild(cloned)
84
+ }
85
+ if (isTableSection) {
86
+ target.appendChild(fragment)
87
+ } else {
88
+ parentElement.appendChild(fragment)
89
+ }
90
+ } else {
91
+ for (let i = start; i < stop; ++i) {
92
+ setNodeValues(target.processNode, data[i], target.dataSlots)
93
+ let cloned = cloneDeep(target.processNode)
94
+ cloned[Config.KEY] = getItemValue(data[i],key)
95
+ insertRow(cloned)
96
+ }
97
+ if (!isTableSection) {
98
+ parentElement.appendChild(target)
99
+ }
100
+ }
101
+ }
102
+
103
+ const renderHTMLWithProvider = (target, dataProvider, start = 0, length = null, rows = []) => {
104
+ if (length === null || length === 0) {
105
+ return
106
+ }
107
+
108
+ const stop = start + length
109
+ const key = target[Config.KEY]
110
+ const insertRow = appendRow.bind(target)
111
+
112
+ const table = target.parentElement
113
+ const wasAttached = table && table.contains(target)
114
+
115
+ if (wasAttached) {
116
+ target.remove()
117
+ }
118
+
119
+ for (let i = start; i < stop; ++i) {
120
+ const dataItem = dataProvider(i, rows)
121
+ setNodeValues(target.processNode, dataItem, target.dataSlots)
122
+ let cloned = cloneDeep(target.processNode)
123
+ cloned[Config.KEY] = getItemValue(dataItem, key)
124
+ insertRow(cloned)
125
+ }
126
+
127
+ if (wasAttached && table) {
128
+ table.append(target)
129
+ }
130
+ }
131
+
132
+ const defaultProvider = (i, list) => list[i]
133
+
134
+ const renderWithProvider = (target, dataList, start = 0, length = null, dataProvider = null) => {
135
+ const provider = dataProvider ?? defaultProvider
136
+ const len = length ?? (dataList?.length ?? 0) - start
137
+ if (len <= 0) return
138
+ renderHTMLWithProvider(target, provider, start, len, dataList ?? [])
139
+ }
140
+
141
+ const add = (target, dataSet, start=0) => {
142
+ renderHTML(target, dataSet, start , dataSet.length - start)
143
+ }
144
+
145
+ const addWithProvider = (target, dataList, start = 0, length = null, dataProvider = null) => {
146
+ const provider = dataProvider ?? defaultProvider
147
+ const len = length ?? (dataList?.length ?? 0) - start
148
+ if (len <= 0) return
149
+
150
+ const stop = start + len
151
+ const key = target[Config.KEY]
152
+ const insertRow = appendRow.bind(target)
153
+ const rows = dataList ?? []
154
+
155
+ // Simple loop - append to existing content (keep attached)
156
+ for (let i = start; i < stop; ++i) {
157
+ // Call provider to get single data object for this index
158
+ const dataItem = provider(i, rows)
159
+
160
+ // Set values and clone the process node
161
+ setNodeValues(target.processNode, dataItem, target.dataSlots)
162
+ let cloned = cloneDeep(target.processNode)
163
+ cloned[Config.KEY] = getItemValue(dataItem, key)
164
+ insertRow(cloned)
165
+ }
166
+ }
167
+
168
+ /**
169
+ * @deprecated Use add() instead. Will be removed in a future version.
170
+ */
171
+ const append = (target, dataSet, start = 0) => {
172
+ if (typeof globalThis !== 'undefined' && globalThis.console?.warn) {
173
+ globalThis.console.warn('DooHTML: append() is deprecated. Use add() instead.')
174
+ }
175
+ return add(target, dataSet, start)
176
+ }
177
+
178
+ /**
179
+ * @deprecated Use addWithProvider() instead. Will be removed in a future version.
180
+ */
181
+ const appendWithProvider = (target, dataList, start = 0, length = null, dataProvider = null) => {
182
+ if (typeof globalThis !== 'undefined' && globalThis.console?.warn) {
183
+ globalThis.console.warn('DooHTML: appendWithProvider() is deprecated. Use addWithProvider() instead.')
184
+ }
185
+ return addWithProvider(target, dataList, start, length, dataProvider)
186
+ }
187
+
188
+ const dooParse = (argDataNode) => {
189
+ const _xAttr = ['src', 'selected', 'checked', 'disabled', 'readonly']
190
+
191
+ let tplNode = cloneDeep(argDataNode)
192
+ tplNode.removeAttribute(Config.DATA_BIND)
193
+ delete tplNode.dataset.key
194
+ let htmlStr = tplNode.outerHTML.replaceAll('\t', '').replaceAll('\n', '')
195
+ let orgStr = htmlStr
196
+ _xAttr.forEach(item => {
197
+ htmlStr = htmlStr.replaceAll(new RegExp(' ' + item + '="{{(.+)}}"', 'g'), ' doo-' + item + '="{{$1}}"')
198
+ })
199
+ let xHtml = (orgStr === htmlStr)
200
+
201
+ let elem = globalThis.document.createElement('template')
202
+ elem.innerHTML = htmlStr
203
+ let dataSlots = []
204
+
205
+ const addDataSlot = (item, fld, type) => {
206
+ let slot = []
207
+ let child = item
208
+ while (child !== elem.firstElementChild) {
209
+ let prev = child.previousSibling
210
+ let cnt = 0
211
+
212
+ while (prev) {
213
+ cnt++
214
+ prev = prev.previousSibling
215
+ }
216
+ child = child.parentNode
217
+ if (child) {
218
+ slot.unshift(cnt)
219
+ }
220
+ }
221
+ dataSlots.push([fld,slot.slice(1),type])
222
+ }
223
+
224
+ const textWalker = globalThis.document.createTreeWalker(
225
+ elem.content,
226
+ globalThis.NodeFilter.SHOW_TEXT,
227
+ {
228
+ acceptNode() {
229
+ return globalThis.NodeFilter.FILTER_ACCEPT
230
+ },
231
+ }
232
+ )
233
+
234
+ let textNode = textWalker.nextNode()
235
+ let multiText = []
236
+ while (textNode) {
237
+ let val = textNode.wholeText.trim()
238
+ if (val.indexOf('{{') === 0 && val.lastIndexOf('}}') === val.length-2) {
239
+ //do nothing
240
+ } else {
241
+ let text = val.replaceAll('{{', '<span>{{').replaceAll('}}', '}}</span>')
242
+ multiText.push({node:textNode.parentNode, oldText:val, newText:text})
243
+
244
+ }
245
+ textNode = textWalker.nextNode()
246
+ }
247
+ for (let i=0, len = multiText.length; i<len; i++) {
248
+ multiText[i].node.innerHTML = multiText[i].node.innerHTML.replace(multiText[i].oldText,multiText[i].newText)
249
+ }
250
+
251
+ let processedElem = cloneDeep(elem.content)
252
+
253
+
254
+ const treeWalker = globalThis.document.createTreeWalker(
255
+ processedElem,
256
+ globalThis.NodeFilter.SHOW_TEXT,
257
+ {
258
+ acceptNode() {
259
+ return globalThis.NodeFilter.FILTER_ACCEPT
260
+ },
261
+ }
262
+ )
263
+
264
+ let currentNode = treeWalker.nextNode()
265
+ while (currentNode) {
266
+ const matches = currentNode.nodeValue.match(/\{\{(.*?)\}\}/g)
267
+ if (matches) {
268
+ const parent = currentNode.parentNode
269
+ matches.forEach((match) => {
270
+ const fld = match.replaceAll(/\{\{|\}\}/g, '').trim()
271
+ const textNode = globalThis.document.createTextNode(fld)
272
+ currentNode.textContent = ''
273
+ const newNode = parent.appendChild(textNode)
274
+ addDataSlot(newNode, fld, 'textContent')
275
+ })
276
+ }
277
+
278
+ currentNode = treeWalker.nextNode()
279
+ }
280
+
281
+ const elemWalker = globalThis.document.createTreeWalker(
282
+ processedElem,
283
+ globalThis.NodeFilter.SHOW_ELEMENT,
284
+ {
285
+ acceptNode() {
286
+ return globalThis.NodeFilter.FILTER_ACCEPT
287
+ }
288
+ }
289
+ )
290
+
291
+ currentNode = elemWalker.nextNode()
292
+ while (currentNode) {
293
+ for (const attr of currentNode.attributes) {
294
+ if (attr.nodeValue.includes('{{')) {
295
+ addDataSlot(currentNode, attr.nodeValue.replace('{{','').replace('}}',''), attr.name)
296
+ }
297
+ }
298
+ currentNode = elemWalker.nextNode()
299
+ }
300
+ let templateStr = processedElem.firstElementChild.outerHTML
301
+ dataSlots.forEach(item=>{
302
+ let str = '{{' + item[0] + '}}'
303
+ templateStr = templateStr.replaceAll(new RegExp(str,'g'),'')
304
+
305
+ })
306
+ processedElem.outerHTML = templateStr
307
+
308
+ return {processNode:processedElem.firstElementChild, xHtml, dataSlots}
309
+ }
310
+
311
+ const fetchTemplate = (url) => {
312
+ return new Promise((resolve, reject) => {
313
+ const xhr = new XMLHttpRequest()
314
+ xhr.open("GET", url)
315
+ xhr.addEventListener('load', () => resolve(xhr.responseText))
316
+ xhr.onerror = () => reject(xhr.statusText)
317
+ xhr.send()
318
+ })
319
+ }
320
+
321
+ const setReactiveDataNodes = (tplNode) => {
322
+ const place = []
323
+ const getNodeLevel = (node) => {
324
+ let level = 0
325
+ while (node.parentElement) {
326
+ node = node.parentElement
327
+ level++
328
+ }
329
+ return level
330
+ }
331
+
332
+ const processReactiveElements = (reactiveElems) => {
333
+ const orderedElems = [...reactiveElems]
334
+ .map((elem) => ({
335
+ elem,
336
+ level: getNodeLevel(elem),
337
+ useParent: elem.dataset.src?.startsWith('this.parent'),
338
+ noRepeat: Object.hasOwn(elem.dataset, 'norepeat'),
339
+ }))
340
+ .sort((a, b) => b.level - a.level)
341
+
342
+ orderedElems.forEach(({ elem, level, useParent, noRepeat }, index) => {
343
+ const parent = elem.parentElement
344
+ // Keep TBODY/THEAD/TFOOT as container so TABLE keeps thead when data-bind is on tbody
345
+ const isTableSection = parent?.tagName === 'TABLE' && '|TBODY|THEAD|TFOOT|'.includes(`|${elem.tagName}|`)
346
+ let dataElem = isTableSection
347
+ ? elem
348
+ : '|STYLE|LINK|'.includes(`|${elem.tagName}|`)
349
+ ? elem
350
+ : parent && '|DL|UL|TBODY|THEAD|TFOOT|TR|SELECT|SECTION|'.includes(`|${parent.tagName}|`)
351
+ ? parent
352
+ : parent || elem // Fallback to elem if parentElement is null
353
+
354
+ // Ensure dataElem is never null
355
+ if (!dataElem) {
356
+ console.warn('dataElem is null, using elem as fallback')
357
+ dataElem = elem
358
+ }
359
+
360
+ const parsedNode = dooParse(elem)
361
+ Object.assign(dataElem, {
362
+ processNode: parsedNode.processNode,
363
+ xHtml: parsedNode.xHtml,
364
+ dataSlots: parsedNode.dataSlots,
365
+ name: index,
366
+ level,
367
+ useParent,
368
+ noRepeat,
369
+ isTable: isTable(dataElem),
370
+ })
371
+
372
+ if (dataElem.tagName === 'DATA' || dataElem.tagName === 'STYLE' || dataElem.tagName === 'LINK') {
373
+ elem.parentElement?.replaceChild(dataElem, elem) || console.warn('Templates should only have one child node')
374
+ }
375
+
376
+ place.push(dataElem)
377
+ })
378
+ }
379
+
380
+ const reactiveElems = tplNode.content.querySelectorAll(`[${Config.DATA_BIND}]`)
381
+ reactiveElems.forEach((elem) => {
382
+ if (!Object.hasOwn(elem.dataset, 'src')) {
383
+ elem.dataset.src = tplNode.hasAttribute('doo-dispatch') ? 'DooX' : Config.DATA_BIND
384
+ }
385
+ delete elem.dataset.src
386
+ })
387
+
388
+ processReactiveElements(reactiveElems)
389
+
390
+ tplNode.place = place
391
+ }
392
+
393
+
394
+ const prefetchTemplate = async (src) => {
395
+ if (src && (src.startsWith('./') || src.startsWith('../') || src.startsWith('http'))) {
396
+ const tpl = await fetchTemplate(src)
397
+ return tpl
398
+ }
399
+ return null
400
+ }
401
+
402
+ const createTemplate = async (id, data = [], src = null) => {
403
+
404
+ let tpl = src ? await prefetchTemplate(src) : ''
405
+ if (!tpl) {
406
+ if (id.startsWith('<')) {
407
+ tpl = id
408
+ } else if (id.startsWith('#')) {
409
+ tpl = globalThis.document.querySelector(id).outerHTML
410
+ } else {
411
+ tpl = globalThis.document.querySelector('#' + id).outerHTML
412
+ }
413
+ }
414
+
415
+ const elem = globalThis.document.createElement('div')
416
+ elem.innerHTML = tpl
417
+ if (elem.querySelector('template')) {
418
+ elem.innerHTML = elem.querySelector('template')
419
+ ? tpl
420
+ : `<template><center><pre>The template you are trying to import does not have a &lt;template&gt; tag</pre><div style="color:red">${tpl}</div></center></template>`
421
+ }
422
+ const importedTemplate = cloneDeep(elem.querySelector('template'))
423
+
424
+ const templateNode = globalThis.document.createElement('template')
425
+ importedTemplate.removeAttribute('id')
426
+ templateNode.innerHTML = importedTemplate.innerHTML
427
+
428
+ setReactiveDataNodes(templateNode)
429
+ const subscriber = globalThis.document.querySelector(`[data-template="${id}"]`)
430
+ templateNode["place"][0].textContent = ''
431
+ templateNode["place"][0][Config.KEY] = subscriber.dataset[Config.KEY]
432
+ subscriber.parentElement.replaceChild(templateNode.content, subscriber)
433
+
434
+ if (data.length > 0) {
435
+ render(templateNode["place"][0], data, 0)
436
+ }
437
+
438
+ return templateNode["place"][0]
439
+ }
440
+ export { createTemplate, add, addWithProvider, append, appendWithProvider, render, renderWithProvider, Config, version, prefetchTemplate }
@@ -0,0 +1 @@
1
+ const Config={t:"DooHTML",l:"data-bind",o:"data-template",i:{T:-1,p:0,h:1},u:{BEG:"{{",END:"}}"},m:"data-key",D:"key"},{cloneNode:cloneNode,appendChild:appendChild}=globalThis.Node.prototype,cloneDeep=e=>cloneNode.call(e,!0),appendRow=function(e){return appendChild.call(this,e)},version="v0.98.9-beta.0",getItemValue=(e,t)=>t.includes(".")?t.split(".").reduce(((e,t)=>e&&void 0!==e[t]?e[t]:""),e):e[t]?e[t]:"",getNode=(e,t)=>t.reduce(((e,t)=>e?.childNodes[t]||null),e),isTable=e=>!(!e||!e.tagName)&&["TABLE","TBODY","THEAD","TFOOT","TR","TH"].includes(e.tagName),setNodeValues=(e,t,l)=>{const o=l.length;for(let a=0;a<o;a++){const o=getNode(e,l[a][1]);o?"textContent"===l[a][2]?o.nodeValue=t[l[a][0]]:o.setAttribute(l[a][2],t[l[a][0]]):globalThis.console.log("Field:"+l[a][0]+" does not exist")}},render=(e,t,l=0)=>{0!==t.length?renderHTML(e,t,l):e.textContent=""},renderHTML=(e,t,l=0,o=null)=>{let a=t.length,n=o?l+a:a-l;n>a&&(n=a),e.textContent="";const d=appendRow.bind(e),s=e.parentElement,r=e[Config.D],i=e.childNodes,T="TBODY"===e.tagName||"THEAD"===e.tagName||"TFOOT"===e.tagName;T||e.remove();let p=globalThis.document.createDocumentFragment();if(i.length>0){for(let o=l;o<n;++o){setNodeValues(e.v,t[o],e.N);let l=cloneDeep(e.v);l[Config.D]=getItemValue(t[o],r),p.appendChild(l)}T?e.appendChild(p):s.appendChild(p)}else{for(let o=l;o<n;++o){setNodeValues(e.v,t[o],e.N);let l=cloneDeep(e.v);l[Config.D]=getItemValue(t[o],r),d(l)}T||s.appendChild(e)}},renderHTMLWithProvider=(e,t,l=0,o=null,a=[])=>{if(null===o||0===o)return;const n=l+o,d=e[Config.D],s=appendRow.bind(e),r=e.parentElement,i=r&&r.contains(e);i&&e.remove();for(let o=l;o<n;++o){const l=t(o,a);setNodeValues(e.v,l,e.N);let n=cloneDeep(e.v);n[Config.D]=getItemValue(l,d),s(n)}i&&r&&r.append(e)},defaultProvider=(e,t)=>t[e],renderWithProvider=(e,t,l=0,o=null,a=null)=>{const n=a??defaultProvider,d=o??(t?.length??0)-l;d<=0||renderHTMLWithProvider(e,n,l,d,t??[])},add=(e,t,l=0)=>{renderHTML(e,t,l,t.length-l)},addWithProvider=(e,t,l=0,o=null,a=null)=>{const n=a??defaultProvider,d=o??(t?.length??0)-l;if(d<=0)return;const s=l+d,r=e[Config.D],i=appendRow.bind(e),T=t??[];for(let t=l;t<s;++t){const l=n(t,T);setNodeValues(e.v,l,e.N);let o=cloneDeep(e.v);o[Config.D]=getItemValue(l,r),i(o)}},append=(e,t,l=0)=>("undefined"!=typeof globalThis&&globalThis.console?.warn&&globalThis.console.warn("DooHTML: append() is deprecated. Use add() instead."),add(e,t,l)),appendWithProvider=(e,t,l=0,o=null,a=null)=>("undefined"!=typeof globalThis&&globalThis.console?.warn&&globalThis.console.warn("DooHTML: appendWithProvider() is deprecated. Use addWithProvider() instead."),addWithProvider(e,t,l,o,a)),dooParse=e=>{let t=cloneDeep(e);t.removeAttribute(Config.l),delete t.dataset.key;let l=t.outerHTML.replaceAll("\t","").replaceAll("\n",""),o=l;["src","selected","checked","disabled","readonly"].forEach((e=>{l=l.replaceAll(new RegExp(" "+e+'="{{(.+)}}"',"g")," doo-"+e+'="{{$1}}"')}));let a=o===l,n=globalThis.document.createElement("template");n.innerHTML=l;let d=[];const s=(e,t,l)=>{let o=[],a=e;for(;a!==n.firstElementChild;){let e=a.previousSibling,t=0;for(;e;)t++,e=e.previousSibling;a=a.parentNode,a&&o.unshift(t)}d.push([t,o.slice(1),l])},r=globalThis.document.createTreeWalker(n.content,globalThis.NodeFilter.SHOW_TEXT,{acceptNode:()=>globalThis.NodeFilter.FILTER_ACCEPT});let i=r.nextNode(),T=[];for(;i;){let e=i.wholeText.trim();if(0===e.indexOf("{{")&&e.lastIndexOf("}}")===e.length-2);else{let t=e.replaceAll("{{","<span>{{").replaceAll("}}","}}</span>");T.push({node:i.parentNode,C:e,P:t})}i=r.nextNode()}for(let e=0,t=T.length;e<t;e++)T[e].node.innerHTML=T[e].node.innerHTML.replace(T[e].C,T[e].P);let p=cloneDeep(n.content);const c=globalThis.document.createTreeWalker(p,globalThis.NodeFilter.SHOW_TEXT,{acceptNode:()=>globalThis.NodeFilter.FILTER_ACCEPT});let g=c.nextNode();for(;g;){const e=g.nodeValue.match(/\{\{(.*?)\}\}/g);if(e){const t=g.parentNode;e.forEach((e=>{const l=e.replaceAll(/\{\{|\}\}/g,"").trim(),o=globalThis.document.createTextNode(l);g.textContent="";const a=t.appendChild(o);s(a,l,"textContent")}))}g=c.nextNode()}const h=globalThis.document.createTreeWalker(p,globalThis.NodeFilter.SHOW_ELEMENT,{acceptNode:()=>globalThis.NodeFilter.FILTER_ACCEPT});for(g=h.nextNode();g;){for(const e of g.attributes)e.nodeValue.includes("{{")&&s(g,e.nodeValue.replace("{{","").replace("}}",""),e.name);g=h.nextNode()}let f=p.firstElementChild.outerHTML;return d.forEach((e=>{let t="{{"+e[0]+"}}";f=f.replaceAll(new RegExp(t,"g"),"")})),p.outerHTML=f,{v:p.firstElementChild,A:a,N:d}},fetchTemplate=e=>new Promise(((t,l)=>{const o=new XMLHttpRequest;o.open("GET",e),o.addEventListener("load",(()=>t(o.responseText))),o.onerror=()=>l(o.statusText),o.send()})),setReactiveDataNodes=e=>{const t=[],l=e=>{let t=0;for(;e.parentElement;)e=e.parentElement,t++;return t},o=e.content.querySelectorAll(`[${Config.l}]`);o.forEach((t=>{Object.hasOwn(t.dataset,"src")||(t.dataset.src=e.hasAttribute("doo-dispatch")?"DooX":Config.l),delete t.dataset.src})),(e=>{[...e].map((e=>({L:e,level:l(e),H:e.dataset.src?.startsWith("this.parent"),O:Object.hasOwn(e.dataset,"norepeat")}))).sort(((e,t)=>t.level-e.level)).forEach((({L:e,level:l,H:o,O:a},n)=>{const d=e.parentElement;let s="TABLE"===d?.tagName&&"|TBODY|THEAD|TFOOT|".includes(`|${e.tagName}|`)||"|STYLE|LINK|".includes(`|${e.tagName}|`)?e:d&&"|DL|UL|TBODY|THEAD|TFOOT|TR|SELECT|SECTION|".includes(`|${d.tagName}|`)?d:d||e;s||(console.warn("dataElem is null, using elem as fallback"),s=e);const r=dooParse(e);Object.assign(s,{v:r.v,A:r.A,N:r.N,name:n,level:l,H:o,O:a,R:isTable(s)}),"DATA"!==s.tagName&&"STYLE"!==s.tagName&&"LINK"!==s.tagName||e.parentElement?.replaceChild(s,e)||console.warn("Templates should only have one child node"),t.push(s)}))})(o),e.place=t},prefetchTemplate=async e=>{if(e&&(e.startsWith("./")||e.startsWith("../")||e.startsWith("http"))){return await fetchTemplate(e)}return null},createTemplate=async(e,t=[],l=null)=>{let o=l?await prefetchTemplate(l):"";o||(o=e.startsWith("<")?e:e.startsWith("#")?globalThis.document.querySelector(e).outerHTML:globalThis.document.querySelector("#"+e).outerHTML);const a=globalThis.document.createElement("div");a.innerHTML=o,a.querySelector("template")&&(a.innerHTML=a.querySelector("template")?o:`<template><center><pre>The template you are trying to import does not have a &lt;template&gt; tag</pre><div style="color:red">${o}</div></center></template>`);const n=cloneDeep(a.querySelector("template")),d=globalThis.document.createElement("template");n.removeAttribute("id"),d.innerHTML=n.innerHTML,setReactiveDataNodes(d);const s=globalThis.document.querySelector(`[data-template="${e}"]`);return d.place[0].textContent="",d.place[0][Config.D]=s.dataset[Config.D],s.parentElement.replaceChild(d.content,s),t.length>0&&render(d.place[0],t,0),d.place[0]};export{createTemplate,add,addWithProvider,append,appendWithProvider,render,renderWithProvider,Config,version,prefetchTemplate};
@@ -0,0 +1,440 @@
1
+ const Config = {
2
+ NAME:'DooHTML',
3
+ DATA_BIND:'data-bind',
4
+ DATA_TEMPLATE:'data-template',
5
+ MATCH:{ANY:-1,STARTS_WITH:0,EXACT:1},
6
+ DELIMITER:{'BEG':'{{','END':'}}'},
7
+ DATA_KEY:'data-key',
8
+ KEY:'key'
9
+ }
10
+
11
+ const {cloneNode, appendChild} = globalThis.Node.prototype
12
+ const cloneDeep = n => cloneNode.call(n, true)
13
+ const appendRow = function(child) {
14
+ return appendChild.call(this, child);
15
+ };
16
+
17
+ const version = 'v0.98.9-beta.0'
18
+
19
+ const getItemValue = (item, prop) => {
20
+ if (!prop.includes('.')) {
21
+ return item[prop] ? item[prop] : ''
22
+ }
23
+
24
+ return prop.split('.').reduce((acc, key) => {
25
+ return acc && acc[key] !== undefined ? acc[key] : ''
26
+ }, item)
27
+ }
28
+
29
+ const getNode = (node, arr) => arr.reduce((currentNode, index) => currentNode?.childNodes[index] || null, node)
30
+
31
+
32
+ const isTable = (node) => {
33
+ if (!node || !node.tagName) return false
34
+ return ['TABLE','TBODY','THEAD','TFOOT','TR','TH'].includes(node.tagName)
35
+ }
36
+
37
+ const setNodeValues = (node, dataItem, dataSlots) => {
38
+ const len = dataSlots.length
39
+ for (let x = 0; x < len; x++) {
40
+ const curNode = getNode(node, dataSlots[x][1], 0)
41
+ if (curNode) {
42
+ if (dataSlots[x][2] === 'textContent') {
43
+ curNode.nodeValue = dataItem[dataSlots[x][0]]
44
+ } else {
45
+ curNode.setAttribute(dataSlots[x][2], dataItem[dataSlots[x][0]])
46
+ }
47
+ } else {
48
+ globalThis.console.log('Field:' + dataSlots[x][0] + ' does not exist')
49
+ }
50
+ }
51
+ }
52
+
53
+ const render = (target, data, start = 0) => {
54
+ if (data.length === 0) {
55
+ target.textContent = ''
56
+ return
57
+ }
58
+ renderHTML(target, data, start)
59
+ }
60
+
61
+ const renderHTML = (target, data, start = 0, end=null) => {
62
+ let dataLen = data.length
63
+
64
+ let stop = end ? start + dataLen : dataLen - start
65
+ if (stop > dataLen) { stop = dataLen }
66
+
67
+ // Clear existing content
68
+ target.textContent = ''
69
+ const insertRow = appendRow.bind(target)
70
+ const parentElement = target.parentElement
71
+ const key = target[Config.KEY]
72
+ const childNodes = target.childNodes
73
+ const isTableSection = target.tagName === 'TBODY' || target.tagName === 'THEAD' || target.tagName === 'TFOOT'
74
+ if (!isTableSection) {
75
+ target.remove()
76
+ }
77
+ let fragment = globalThis.document.createDocumentFragment()
78
+ if (childNodes.length > 0) {
79
+ for (let i = start; i < stop; ++i) {
80
+ setNodeValues(target.processNode, data[i], target.dataSlots)
81
+ let cloned = cloneDeep(target.processNode)
82
+ cloned[Config.KEY] = getItemValue(data[i],key)
83
+ fragment.appendChild(cloned)
84
+ }
85
+ if (isTableSection) {
86
+ target.appendChild(fragment)
87
+ } else {
88
+ parentElement.appendChild(fragment)
89
+ }
90
+ } else {
91
+ for (let i = start; i < stop; ++i) {
92
+ setNodeValues(target.processNode, data[i], target.dataSlots)
93
+ let cloned = cloneDeep(target.processNode)
94
+ cloned[Config.KEY] = getItemValue(data[i],key)
95
+ insertRow(cloned)
96
+ }
97
+ if (!isTableSection) {
98
+ parentElement.appendChild(target)
99
+ }
100
+ }
101
+ }
102
+
103
+ const renderHTMLWithProvider = (target, dataProvider, start = 0, length = null, rows = []) => {
104
+ if (length === null || length === 0) {
105
+ return
106
+ }
107
+
108
+ const stop = start + length
109
+ const key = target[Config.KEY]
110
+ const insertRow = appendRow.bind(target)
111
+
112
+ const table = target.parentElement
113
+ const wasAttached = table && table.contains(target)
114
+
115
+ if (wasAttached) {
116
+ target.remove()
117
+ }
118
+
119
+ for (let i = start; i < stop; ++i) {
120
+ const dataItem = dataProvider(i, rows)
121
+ setNodeValues(target.processNode, dataItem, target.dataSlots)
122
+ let cloned = cloneDeep(target.processNode)
123
+ cloned[Config.KEY] = getItemValue(dataItem, key)
124
+ insertRow(cloned)
125
+ }
126
+
127
+ if (wasAttached && table) {
128
+ table.append(target)
129
+ }
130
+ }
131
+
132
+ const defaultProvider = (i, list) => list[i]
133
+
134
+ const renderWithProvider = (target, dataList, start = 0, length = null, dataProvider = null) => {
135
+ const provider = dataProvider ?? defaultProvider
136
+ const len = length ?? (dataList?.length ?? 0) - start
137
+ if (len <= 0) return
138
+ renderHTMLWithProvider(target, provider, start, len, dataList ?? [])
139
+ }
140
+
141
+ const add = (target, dataSet, start=0) => {
142
+ renderHTML(target, dataSet, start , dataSet.length - start)
143
+ }
144
+
145
+ const addWithProvider = (target, dataList, start = 0, length = null, dataProvider = null) => {
146
+ const provider = dataProvider ?? defaultProvider
147
+ const len = length ?? (dataList?.length ?? 0) - start
148
+ if (len <= 0) return
149
+
150
+ const stop = start + len
151
+ const key = target[Config.KEY]
152
+ const insertRow = appendRow.bind(target)
153
+ const rows = dataList ?? []
154
+
155
+ // Simple loop - append to existing content (keep attached)
156
+ for (let i = start; i < stop; ++i) {
157
+ // Call provider to get single data object for this index
158
+ const dataItem = provider(i, rows)
159
+
160
+ // Set values and clone the process node
161
+ setNodeValues(target.processNode, dataItem, target.dataSlots)
162
+ let cloned = cloneDeep(target.processNode)
163
+ cloned[Config.KEY] = getItemValue(dataItem, key)
164
+ insertRow(cloned)
165
+ }
166
+ }
167
+
168
+ /**
169
+ * @deprecated Use add() instead. Will be removed in a future version.
170
+ */
171
+ const append = (target, dataSet, start = 0) => {
172
+ if (typeof globalThis !== 'undefined' && globalThis.console?.warn) {
173
+ globalThis.console.warn('DooHTML: append() is deprecated. Use add() instead.')
174
+ }
175
+ return add(target, dataSet, start)
176
+ }
177
+
178
+ /**
179
+ * @deprecated Use addWithProvider() instead. Will be removed in a future version.
180
+ */
181
+ const appendWithProvider = (target, dataList, start = 0, length = null, dataProvider = null) => {
182
+ if (typeof globalThis !== 'undefined' && globalThis.console?.warn) {
183
+ globalThis.console.warn('DooHTML: appendWithProvider() is deprecated. Use addWithProvider() instead.')
184
+ }
185
+ return addWithProvider(target, dataList, start, length, dataProvider)
186
+ }
187
+
188
+ const dooParse = (argDataNode) => {
189
+ const _xAttr = ['src', 'selected', 'checked', 'disabled', 'readonly']
190
+
191
+ let tplNode = cloneDeep(argDataNode)
192
+ tplNode.removeAttribute(Config.DATA_BIND)
193
+ delete tplNode.dataset.key
194
+ let htmlStr = tplNode.outerHTML.replaceAll('\t', '').replaceAll('\n', '')
195
+ let orgStr = htmlStr
196
+ _xAttr.forEach(item => {
197
+ htmlStr = htmlStr.replaceAll(new RegExp(' ' + item + '="{{(.+)}}"', 'g'), ' doo-' + item + '="{{$1}}"')
198
+ })
199
+ let xHtml = (orgStr === htmlStr)
200
+
201
+ let elem = globalThis.document.createElement('template')
202
+ elem.innerHTML = htmlStr
203
+ let dataSlots = []
204
+
205
+ const addDataSlot = (item, fld, type) => {
206
+ let slot = []
207
+ let child = item
208
+ while (child !== elem.firstElementChild) {
209
+ let prev = child.previousSibling
210
+ let cnt = 0
211
+
212
+ while (prev) {
213
+ cnt++
214
+ prev = prev.previousSibling
215
+ }
216
+ child = child.parentNode
217
+ if (child) {
218
+ slot.unshift(cnt)
219
+ }
220
+ }
221
+ dataSlots.push([fld,slot.slice(1),type])
222
+ }
223
+
224
+ const textWalker = globalThis.document.createTreeWalker(
225
+ elem.content,
226
+ globalThis.NodeFilter.SHOW_TEXT,
227
+ {
228
+ acceptNode() {
229
+ return globalThis.NodeFilter.FILTER_ACCEPT
230
+ },
231
+ }
232
+ )
233
+
234
+ let textNode = textWalker.nextNode()
235
+ let multiText = []
236
+ while (textNode) {
237
+ let val = textNode.wholeText.trim()
238
+ if (val.indexOf('{{') === 0 && val.lastIndexOf('}}') === val.length-2) {
239
+ //do nothing
240
+ } else {
241
+ let text = val.replaceAll('{{', '<span>{{').replaceAll('}}', '}}</span>')
242
+ multiText.push({node:textNode.parentNode, oldText:val, newText:text})
243
+
244
+ }
245
+ textNode = textWalker.nextNode()
246
+ }
247
+ for (let i=0, len = multiText.length; i<len; i++) {
248
+ multiText[i].node.innerHTML = multiText[i].node.innerHTML.replace(multiText[i].oldText,multiText[i].newText)
249
+ }
250
+
251
+ let processedElem = cloneDeep(elem.content)
252
+
253
+
254
+ const treeWalker = globalThis.document.createTreeWalker(
255
+ processedElem,
256
+ globalThis.NodeFilter.SHOW_TEXT,
257
+ {
258
+ acceptNode() {
259
+ return globalThis.NodeFilter.FILTER_ACCEPT
260
+ },
261
+ }
262
+ )
263
+
264
+ let currentNode = treeWalker.nextNode()
265
+ while (currentNode) {
266
+ const matches = currentNode.nodeValue.match(/\{\{(.*?)\}\}/g)
267
+ if (matches) {
268
+ const parent = currentNode.parentNode
269
+ matches.forEach((match) => {
270
+ const fld = match.replaceAll(/\{\{|\}\}/g, '').trim()
271
+ const textNode = globalThis.document.createTextNode(fld)
272
+ currentNode.textContent = ''
273
+ const newNode = parent.appendChild(textNode)
274
+ addDataSlot(newNode, fld, 'textContent')
275
+ })
276
+ }
277
+
278
+ currentNode = treeWalker.nextNode()
279
+ }
280
+
281
+ const elemWalker = globalThis.document.createTreeWalker(
282
+ processedElem,
283
+ globalThis.NodeFilter.SHOW_ELEMENT,
284
+ {
285
+ acceptNode() {
286
+ return globalThis.NodeFilter.FILTER_ACCEPT
287
+ }
288
+ }
289
+ )
290
+
291
+ currentNode = elemWalker.nextNode()
292
+ while (currentNode) {
293
+ for (const attr of currentNode.attributes) {
294
+ if (attr.nodeValue.includes('{{')) {
295
+ addDataSlot(currentNode, attr.nodeValue.replace('{{','').replace('}}',''), attr.name)
296
+ }
297
+ }
298
+ currentNode = elemWalker.nextNode()
299
+ }
300
+ let templateStr = processedElem.firstElementChild.outerHTML
301
+ dataSlots.forEach(item=>{
302
+ let str = '{{' + item[0] + '}}'
303
+ templateStr = templateStr.replaceAll(new RegExp(str,'g'),'')
304
+
305
+ })
306
+ processedElem.outerHTML = templateStr
307
+
308
+ return {processNode:processedElem.firstElementChild, xHtml, dataSlots}
309
+ }
310
+
311
+ const fetchTemplate = (url) => {
312
+ return new Promise((resolve, reject) => {
313
+ const xhr = new XMLHttpRequest()
314
+ xhr.open("GET", url)
315
+ xhr.addEventListener('load', () => resolve(xhr.responseText))
316
+ xhr.onerror = () => reject(xhr.statusText)
317
+ xhr.send()
318
+ })
319
+ }
320
+
321
+ const setReactiveDataNodes = (tplNode) => {
322
+ const place = []
323
+ const getNodeLevel = (node) => {
324
+ let level = 0
325
+ while (node.parentElement) {
326
+ node = node.parentElement
327
+ level++
328
+ }
329
+ return level
330
+ }
331
+
332
+ const processReactiveElements = (reactiveElems) => {
333
+ const orderedElems = [...reactiveElems]
334
+ .map((elem) => ({
335
+ elem,
336
+ level: getNodeLevel(elem),
337
+ useParent: elem.dataset.src?.startsWith('this.parent'),
338
+ noRepeat: Object.hasOwn(elem.dataset, 'norepeat'),
339
+ }))
340
+ .sort((a, b) => b.level - a.level)
341
+
342
+ orderedElems.forEach(({ elem, level, useParent, noRepeat }, index) => {
343
+ const parent = elem.parentElement
344
+ // Keep TBODY/THEAD/TFOOT as container so TABLE keeps thead when data-bind is on tbody
345
+ const isTableSection = parent?.tagName === 'TABLE' && '|TBODY|THEAD|TFOOT|'.includes(`|${elem.tagName}|`)
346
+ let dataElem = isTableSection
347
+ ? elem
348
+ : '|STYLE|LINK|'.includes(`|${elem.tagName}|`)
349
+ ? elem
350
+ : parent && '|DL|UL|TBODY|THEAD|TFOOT|TR|SELECT|SECTION|'.includes(`|${parent.tagName}|`)
351
+ ? parent
352
+ : parent || elem // Fallback to elem if parentElement is null
353
+
354
+ // Ensure dataElem is never null
355
+ if (!dataElem) {
356
+ console.warn('dataElem is null, using elem as fallback')
357
+ dataElem = elem
358
+ }
359
+
360
+ const parsedNode = dooParse(elem)
361
+ Object.assign(dataElem, {
362
+ processNode: parsedNode.processNode,
363
+ xHtml: parsedNode.xHtml,
364
+ dataSlots: parsedNode.dataSlots,
365
+ name: index,
366
+ level,
367
+ useParent,
368
+ noRepeat,
369
+ isTable: isTable(dataElem),
370
+ })
371
+
372
+ if (dataElem.tagName === 'DATA' || dataElem.tagName === 'STYLE' || dataElem.tagName === 'LINK') {
373
+ elem.parentElement?.replaceChild(dataElem, elem) || console.warn('Templates should only have one child node')
374
+ }
375
+
376
+ place.push(dataElem)
377
+ })
378
+ }
379
+
380
+ const reactiveElems = tplNode.content.querySelectorAll(`[${Config.DATA_BIND}]`)
381
+ reactiveElems.forEach((elem) => {
382
+ if (!Object.hasOwn(elem.dataset, 'src')) {
383
+ elem.dataset.src = tplNode.hasAttribute('doo-dispatch') ? 'DooX' : Config.DATA_BIND
384
+ }
385
+ delete elem.dataset.src
386
+ })
387
+
388
+ processReactiveElements(reactiveElems)
389
+
390
+ tplNode.place = place
391
+ }
392
+
393
+
394
+ const prefetchTemplate = async (src) => {
395
+ if (src && (src.startsWith('./') || src.startsWith('../') || src.startsWith('http'))) {
396
+ const tpl = await fetchTemplate(src)
397
+ return tpl
398
+ }
399
+ return null
400
+ }
401
+
402
+ const createTemplate = async (id, data = [], src = null) => {
403
+
404
+ let tpl = src ? await prefetchTemplate(src) : ''
405
+ if (!tpl) {
406
+ if (id.startsWith('<')) {
407
+ tpl = id
408
+ } else if (id.startsWith('#')) {
409
+ tpl = globalThis.document.querySelector(id).outerHTML
410
+ } else {
411
+ tpl = globalThis.document.querySelector('#' + id).outerHTML
412
+ }
413
+ }
414
+
415
+ const elem = globalThis.document.createElement('div')
416
+ elem.innerHTML = tpl
417
+ if (elem.querySelector('template')) {
418
+ elem.innerHTML = elem.querySelector('template')
419
+ ? tpl
420
+ : `<template><center><pre>The template you are trying to import does not have a &lt;template&gt; tag</pre><div style="color:red">${tpl}</div></center></template>`
421
+ }
422
+ const importedTemplate = cloneDeep(elem.querySelector('template'))
423
+
424
+ const templateNode = globalThis.document.createElement('template')
425
+ importedTemplate.removeAttribute('id')
426
+ templateNode.innerHTML = importedTemplate.innerHTML
427
+
428
+ setReactiveDataNodes(templateNode)
429
+ const subscriber = globalThis.document.querySelector(`[data-template="${id}"]`)
430
+ templateNode["place"][0].textContent = ''
431
+ templateNode["place"][0][Config.KEY] = subscriber.dataset[Config.KEY]
432
+ subscriber.parentElement.replaceChild(templateNode.content, subscriber)
433
+
434
+ if (data.length > 0) {
435
+ render(templateNode["place"][0], data, 0)
436
+ }
437
+
438
+ return templateNode["place"][0]
439
+ }
440
+ export { createTemplate, add, addWithProvider, append, appendWithProvider, render, renderWithProvider, Config, version, prefetchTemplate }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "doohtml",
3
+ "version": "0.98.9-beta.0",
4
+ "type": "module",
5
+ "description": "DooHTML is a JavaScript library for creating HTML elements and templates using high performant rendering methods",
6
+ "main": "dist/doohtml.js",
7
+ "module": "dist/doohtml.mjs",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/doohtml.mjs",
11
+ "require": "./dist/doohtml.js",
12
+ "default": "./dist/doohtml.mjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "npm run build:copy && npm run build:min",
21
+ "build:copy": "mkdir -p dist && cp src/doohtml.js dist/doohtml.js && cp src/doohtml.js dist/doohtml.mjs",
22
+ "build:min": "terser src/doohtml.js -c --mangle --mangle-props keep_quoted -o dist/doohtml.min.mjs",
23
+ "preview": "npm run build && npm pack",
24
+ "prepublishOnly": "npm run build",
25
+ "test": "node test/doohtml.test.js",
26
+ "serve": "serve . -l 4000"
27
+ },
28
+ "author": "Henrik Javen",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/hman61/doohtml.git"
33
+ },
34
+ "keywords": [
35
+ "html",
36
+ "template",
37
+ "rendering",
38
+ "dom",
39
+ "framework"
40
+ ],
41
+ "devDependencies": {
42
+ "eslint": "^9.39.2",
43
+ "eslint-plugin-unicorn": "^62.0.0",
44
+ "jsdom": "^25.0.0",
45
+ "serve": "^14.2.5",
46
+ "terser": "^5.20.2"
47
+ }
48
+ }