jq79 0.1.0 → 0.1.3
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 +258 -0
- package/dist/jq79.cjs +5 -0
- package/dist/jq79.cjs.map +1 -0
- package/dist/jq79.d.ts +53 -0
- package/dist/jq79.global.js +5 -0
- package/dist/jq79.global.js.map +1 -0
- package/dist/jq79.js +5 -0
- package/dist/jq79.js.map +1 -0
- package/package.json +33 -9
- package/src/jq79.ts +964 -0
- package/dist/jk79.js +0 -0
- package/src/jk79.test.ts +0 -179
- package/src/jk79.ts +0 -144
- package/vitest.config.ts +0 -7
package/dist/jk79.js
DELETED
|
File without changes
|
package/src/jk79.test.ts
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from "vitest"
|
|
2
|
-
import { $, $$, parseComponent, createReactiveDeepData } from "./jk79"
|
|
3
|
-
|
|
4
|
-
describe("$", () => {
|
|
5
|
-
beforeEach(() => {
|
|
6
|
-
document.body.innerHTML = `
|
|
7
|
-
<div id="root">
|
|
8
|
-
<span class="item">a</span>
|
|
9
|
-
<span class="item">b</span>
|
|
10
|
-
</div>
|
|
11
|
-
`
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it("queries the document when given a selector string", () => {
|
|
15
|
-
expect($("#root")).not.toBeNull()
|
|
16
|
-
expect($("#root")?.tagName).toBe("DIV")
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it("returns null when a selector string matches nothing", () => {
|
|
20
|
-
expect($("#missing")).toBeNull()
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it("queries within an element when given an element + selector", () => {
|
|
24
|
-
const root = document.getElementById("root")!
|
|
25
|
-
expect($(root, ".item")?.textContent).toBe("a")
|
|
26
|
-
})
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
describe("$$", () => {
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
document.body.innerHTML = `
|
|
32
|
-
<div id="root">
|
|
33
|
-
<span class="item">a</span>
|
|
34
|
-
<span class="item">b</span>
|
|
35
|
-
</div>
|
|
36
|
-
`
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it("returns an array of all matches for a selector string", () => {
|
|
40
|
-
const items = $$(".item")
|
|
41
|
-
expect(Array.isArray(items)).toBe(true)
|
|
42
|
-
expect(items).toHaveLength(2)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it("returns an empty array when nothing matches", () => {
|
|
46
|
-
expect($$(".missing")).toEqual([])
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it("returns matches scoped to an element when given an element + selector", () => {
|
|
50
|
-
const root = document.getElementById("root")!
|
|
51
|
-
const items = $$(root, ".item")
|
|
52
|
-
expect(items).toHaveLength(2)
|
|
53
|
-
expect(items.map(el => el.textContent)).toEqual(["a", "b"])
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
describe("parseComponent", () => {
|
|
58
|
-
const component = `
|
|
59
|
-
<script :setup="{ fname, lname }">
|
|
60
|
-
const fullName = \`\${fname} \${lname}\`
|
|
61
|
-
</script>
|
|
62
|
-
|
|
63
|
-
<div :bind="{ fullName }"></div>
|
|
64
|
-
<div class="full-name">
|
|
65
|
-
{{ fullName }}
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
|
-
<style>
|
|
69
|
-
.full-name {
|
|
70
|
-
color: red;
|
|
71
|
-
}
|
|
72
|
-
</style>
|
|
73
|
-
`
|
|
74
|
-
|
|
75
|
-
it("extracts scripts with their attrs and content", () => {
|
|
76
|
-
const { scripts } = parseComponent(component)
|
|
77
|
-
|
|
78
|
-
expect(scripts).toHaveLength(1)
|
|
79
|
-
expect(scripts[0].attrs).toEqual({ ":setup": "{ fname, lname }" })
|
|
80
|
-
expect(scripts[0].content).toContain("const fullName")
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it("extracts styles with their attrs and content", () => {
|
|
84
|
-
const { styles } = parseComponent(component)
|
|
85
|
-
|
|
86
|
-
expect(styles).toHaveLength(1)
|
|
87
|
-
expect(styles[0].attrs).toEqual({})
|
|
88
|
-
expect(styles[0].content).toContain(".full-name")
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it("builds a template AST excluding script/style tags", () => {
|
|
92
|
-
const { template } = parseComponent(component)
|
|
93
|
-
|
|
94
|
-
expect(template).toHaveLength(2)
|
|
95
|
-
|
|
96
|
-
expect(template[0]).toEqual({
|
|
97
|
-
tag: "div",
|
|
98
|
-
attrs: { ":bind": "{ fullName }" },
|
|
99
|
-
children: [],
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
expect(template[1]).toEqual({
|
|
103
|
-
tag: "div",
|
|
104
|
-
attrs: { class: "full-name" },
|
|
105
|
-
children: ["{{ fullName }}"],
|
|
106
|
-
})
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it("recurses into nested elements", () => {
|
|
110
|
-
const { template } = parseComponent(`
|
|
111
|
-
<ul>
|
|
112
|
-
<li>one</li>
|
|
113
|
-
<li>two</li>
|
|
114
|
-
</ul>
|
|
115
|
-
`)
|
|
116
|
-
|
|
117
|
-
expect(template).toEqual([
|
|
118
|
-
{
|
|
119
|
-
tag: "ul",
|
|
120
|
-
attrs: {},
|
|
121
|
-
children: [
|
|
122
|
-
{ tag: "li", attrs: {}, children: ["one"] },
|
|
123
|
-
{ tag: "li", attrs: {}, children: ["two"] },
|
|
124
|
-
],
|
|
125
|
-
},
|
|
126
|
-
])
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it("returns empty collections for a component with no scripts/styles", () => {
|
|
130
|
-
const { scripts, styles } = parseComponent("<div>just a div</div>")
|
|
131
|
-
|
|
132
|
-
expect(scripts).toEqual([])
|
|
133
|
-
expect(styles).toEqual([])
|
|
134
|
-
})
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
describe("createReactiveDeepData", () => {
|
|
138
|
-
it("emits the top-level key on a shallow set", () => {
|
|
139
|
-
const onChange = vi.fn()
|
|
140
|
-
const state = createReactiveDeepData({ name: "a" }, onChange)
|
|
141
|
-
|
|
142
|
-
state.name = "b"
|
|
143
|
-
|
|
144
|
-
expect(onChange).toHaveBeenCalledWith("name", "b")
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it("emits a dot path when setting a property on a nested object present at creation", () => {
|
|
148
|
-
const onChange = vi.fn()
|
|
149
|
-
const state = createReactiveDeepData({ user: { address: { city: "NYC" } } }, onChange)
|
|
150
|
-
|
|
151
|
-
state.user.address.city = "LA"
|
|
152
|
-
|
|
153
|
-
expect(onChange).toHaveBeenCalledWith("user.address.city", "LA")
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it("emits a dot path when setting a property on an object assigned after creation", () => {
|
|
157
|
-
const onChange = vi.fn()
|
|
158
|
-
const state = createReactiveDeepData({ user: null as any }, onChange)
|
|
159
|
-
|
|
160
|
-
state.user = { address: { city: "NYC" } }
|
|
161
|
-
onChange.mockClear()
|
|
162
|
-
|
|
163
|
-
state.user.address.city = "LA"
|
|
164
|
-
|
|
165
|
-
expect(onChange).toHaveBeenCalledWith("user.address.city", "LA")
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
it("keeps deeper paths reactive after a whole subtree is replaced", () => {
|
|
169
|
-
const onChange = vi.fn()
|
|
170
|
-
const state = createReactiveDeepData({ user: { address: { city: "NYC" } } }, onChange)
|
|
171
|
-
|
|
172
|
-
state.user.address = { city: "LA" }
|
|
173
|
-
onChange.mockClear()
|
|
174
|
-
|
|
175
|
-
state.user.address.city = "SF"
|
|
176
|
-
|
|
177
|
-
expect(onChange).toHaveBeenCalledWith("user.address.city", "SF")
|
|
178
|
-
})
|
|
179
|
-
})
|
package/src/jk79.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
export const $ = (selectorOrEl: string | Element, selector?: string) =>
|
|
3
|
-
typeof selectorOrEl === "string"
|
|
4
|
-
? document.querySelector(selectorOrEl)
|
|
5
|
-
: selectorOrEl.querySelector(selector || "")
|
|
6
|
-
|
|
7
|
-
export const $$ = (selectorOrEl: string | Element, selector?: string) => Array.from(
|
|
8
|
-
typeof selectorOrEl === "string"
|
|
9
|
-
? document.querySelectorAll(selectorOrEl)
|
|
10
|
-
: selectorOrEl.querySelectorAll(selector || "")
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
type TemplateNode = {
|
|
14
|
-
tag: string
|
|
15
|
-
attrs: Record<string, string>
|
|
16
|
-
children: (TemplateNode | string)[]
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
type TagBlock = {
|
|
20
|
-
attrs: Record<string, string>
|
|
21
|
-
content: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const elementAttrs = (el: Element): Record<string, string> =>
|
|
25
|
-
Object.fromEntries(Array.from(el.attributes).map(attr => [attr.name, attr.value]))
|
|
26
|
-
|
|
27
|
-
const elementToAST = (el: Element): TemplateNode => ({
|
|
28
|
-
tag: el.tagName.toLowerCase(),
|
|
29
|
-
attrs: elementAttrs(el),
|
|
30
|
-
children: Array.from(el.childNodes).flatMap((node): (TemplateNode | string)[] => {
|
|
31
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
32
|
-
const text = node.textContent?.trim() ?? ""
|
|
33
|
-
return text ? [text] : []
|
|
34
|
-
}
|
|
35
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
36
|
-
return [elementToAST(node as Element)]
|
|
37
|
-
}
|
|
38
|
-
return []
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
const interpolate = (template: string, data: Record<string, any>): string =>
|
|
43
|
-
template.replace(/{{\s*(\w+)\s*}}/g, (_, key) => data[key] ?? "")
|
|
44
|
-
|
|
45
|
-
export const createReactiveDeepData = (
|
|
46
|
-
data: Record<string, any>,
|
|
47
|
-
onChange: (dotKey: string, value: any) => void,
|
|
48
|
-
path = ""
|
|
49
|
-
): Record<string, any> => {
|
|
50
|
-
Object.entries(data).forEach(([key, value]) => {
|
|
51
|
-
if (value && typeof value === "object") {
|
|
52
|
-
data[key] = createReactiveDeepData(value, onChange, path ? `${path}.${key}` : key)
|
|
53
|
-
}
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
return new Proxy(data, {
|
|
57
|
-
set(target, key: string, value) {
|
|
58
|
-
const dotKey = path ? `${path}.${key}` : key
|
|
59
|
-
if (value && typeof value === "object") {
|
|
60
|
-
value = createReactiveDeepData(value, onChange, dotKey)
|
|
61
|
-
}
|
|
62
|
-
target[key] = value
|
|
63
|
-
onChange(dotKey, value)
|
|
64
|
-
return true
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const renderComponent = (component: Component79, data: Record<string, any> = {}): Node => {
|
|
70
|
-
const fragment = document.createDocumentFragment()
|
|
71
|
-
|
|
72
|
-
component.template.forEach(node => {
|
|
73
|
-
const el = document.createElement(node.tag)
|
|
74
|
-
Object.entries(node.attrs).forEach(([key, value]) => {
|
|
75
|
-
el.setAttribute(key, value)
|
|
76
|
-
})
|
|
77
|
-
node.children.forEach(child => {
|
|
78
|
-
if (typeof child === "string") {
|
|
79
|
-
el.appendChild(document.createTextNode(interpolate(child, data)))
|
|
80
|
-
} else {
|
|
81
|
-
el.appendChild(renderComponent(new Component79([child], [], []), data))
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
fragment.appendChild(el)
|
|
85
|
-
})
|
|
86
|
-
return fragment
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
class Component79 {
|
|
90
|
-
constructor(public template: TemplateNode[], public scripts: TagBlock[], public styles: TagBlock[]) {
|
|
91
|
-
this.template = template
|
|
92
|
-
this.scripts = scripts
|
|
93
|
-
this.styles = styles
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// converts a string of HTML into a AST (Abstract Syntax Tree) representation of the component
|
|
98
|
-
// Returns an object with the following properties:
|
|
99
|
-
// - template: the template of the component in a AST representation
|
|
100
|
-
// - scripts: {
|
|
101
|
-
// attrs: the attributes of the script tag
|
|
102
|
-
// content: the content of the script tag
|
|
103
|
-
// }[]
|
|
104
|
-
// - styles: {
|
|
105
|
-
// attrs: the attributes of the style tag
|
|
106
|
-
// content: the content of the style tag
|
|
107
|
-
// }[]
|
|
108
|
-
export const parseComponent = (component: string) => {
|
|
109
|
-
// example
|
|
110
|
-
// <script :setup="{ fname, lname }">
|
|
111
|
-
// const fullName = `${fname} ${lname}`
|
|
112
|
-
// </script>
|
|
113
|
-
//
|
|
114
|
-
// <div :bind="{ fullName }" />
|
|
115
|
-
// <div class="full-name">
|
|
116
|
-
// {{ fullName }}
|
|
117
|
-
// </div>
|
|
118
|
-
//
|
|
119
|
-
// <style>
|
|
120
|
-
// .full-name {
|
|
121
|
-
// color: red;
|
|
122
|
-
// }
|
|
123
|
-
// </style>
|
|
124
|
-
|
|
125
|
-
// parsed as the content of a <template> so leading <script>/<style> tags
|
|
126
|
-
// aren't reparented into <head> by the HTML parser
|
|
127
|
-
const parsedDOM = new DOMParser().parseFromString(`<template>${component}</template>`, "text/html")
|
|
128
|
-
const root = parsedDOM.querySelector("template") as HTMLTemplateElement
|
|
129
|
-
|
|
130
|
-
const scripts: TagBlock[] = []
|
|
131
|
-
const styles: TagBlock[] = []
|
|
132
|
-
const template: TemplateNode[] = []
|
|
133
|
-
|
|
134
|
-
Array.from(root.content.children).forEach(el => {
|
|
135
|
-
const block = { attrs: elementAttrs(el), content: el.textContent ?? "" }
|
|
136
|
-
|
|
137
|
-
if (el.tagName === "SCRIPT") scripts.push(block)
|
|
138
|
-
else if (el.tagName === "STYLE") styles.push(block)
|
|
139
|
-
else template.push(elementToAST(el))
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
return new Component79(template, scripts, styles)
|
|
143
|
-
}
|
|
144
|
-
|