cellery 1.1.1 → 1.3.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/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  const Cellery = require('./lib/cellery')
2
2
  const cells = require('./lib/cells')
3
+ const template = require('./lib/template')
3
4
  const decoration = require('./lib/decorations')
4
5
 
5
6
  module.exports = {
6
7
  Cellery,
7
8
  ...cells,
8
- ...decoration
9
+ ...decoration,
10
+ ...template
9
11
  }
package/lib/cells.js CHANGED
@@ -1,3 +1,5 @@
1
+ const { parse, walk, generate } = require('css-tree/dist/csstree.esm')
2
+
1
3
  class Cell {
2
4
  constructor(opts = {}) {
3
5
  this.id = opts.id
@@ -10,6 +12,7 @@ class Cell {
10
12
  this.decoration = opts.decoration
11
13
  this.size = opts.size
12
14
  this.onclick = opts.onclick
15
+ this.style = opts.style
13
16
  }
14
17
 
15
18
  static Styled(styledOpts = {}) {
@@ -64,6 +67,83 @@ class Cell {
64
67
  }
65
68
  }
66
69
 
70
+ class Style {
71
+ constructor(opts = {}) {
72
+ this.content = parse(opts.children?.join('\n'))
73
+ }
74
+
75
+ findPropertyOfCell(name, property) {
76
+ return this.findProperty(`[data-cellery-cell="${name}"]`, property)
77
+ }
78
+
79
+ findProperty(selector, property) {
80
+ let value = null
81
+
82
+ walk(this.content, {
83
+ visit: 'Rule',
84
+ enter(rule) {
85
+ const prelude = generate(rule.prelude)
86
+ if (prelude !== selector) return
87
+
88
+ rule.block.children.forEach((decl) => {
89
+ if (decl.type === 'Declaration' && decl.property === property) {
90
+ value = generate(decl.value)
91
+ }
92
+ })
93
+ }
94
+ })
95
+
96
+ return value
97
+ }
98
+
99
+ addScope(target, parent) {
100
+ walk(this.content, {
101
+ visit: 'Rule',
102
+ enter(rule) {
103
+ rule.prelude.children.forEach((selector) => {
104
+ const str = generate(selector)
105
+ if (!str.startsWith(target)) return
106
+
107
+ const items = [...selector.children]
108
+ let inserted = false
109
+ for (let i = 0; i < items.length; i++) {
110
+ const node = items[i]
111
+ if (node.type === 'PseudoClassSelector' || node.type === 'PseudoElementSelector') {
112
+ selector.children.insertData({ type: 'IdSelector', name: parent }, items[i])
113
+ inserted = true
114
+ break
115
+ }
116
+ }
117
+ if (!inserted) {
118
+ selector.children.appendData({ type: 'IdSelector', name: parent })
119
+ }
120
+ })
121
+ }
122
+ })
123
+ }
124
+ }
125
+
126
+ class StyleHTML extends Style {
127
+ constructor(opts = {}) {
128
+ super(opts)
129
+
130
+ const cellNames = new Set(Object.keys(opts.cells))
131
+
132
+ walk(this.content, {
133
+ visit: 'TypeSelector',
134
+ enter(node) {
135
+ if (cellNames.has(node.name)) {
136
+ node.name = `[data-cellery-cell="${node.name}"]`
137
+ }
138
+ }
139
+ })
140
+ }
141
+
142
+ toCSS() {
143
+ return generate(this.content)
144
+ }
145
+ }
146
+
67
147
  class Container extends Cell {
68
148
  // TODO: replace with classes
69
149
  static ScrollAll = 'all'
@@ -89,7 +169,7 @@ class App extends Cell {
89
169
  class Text extends Cell {
90
170
  constructor(opts = {}) {
91
171
  super(opts)
92
- this.value = opts.value || ''
172
+ this.value = opts.value || this.children.filter((c) => typeof c === 'string').join('') || ''
93
173
  }
94
174
  }
95
175
 
@@ -114,5 +194,8 @@ module.exports = {
114
194
  App,
115
195
  Text,
116
196
  Paragraph,
117
- Input
197
+ Input,
198
+
199
+ Style,
200
+ StyleHTML
118
201
  }
@@ -0,0 +1,188 @@
1
+ const { Cell, Container, App, Text, Paragraph, Input, StyleHTML, Style } = require('./cells')
2
+
3
+ const SENTINEL = '\x00'
4
+
5
+ const registry = { Cell, Container, App, Text, Paragraph, Input, 'Style.HTML': StyleHTML }
6
+
7
+ function register(cells) {
8
+ Object.assign(registry, cells)
9
+ }
10
+
11
+ function cellery(strings, ...values) {
12
+ let raw = ''
13
+ const slots = []
14
+ for (let i = 0; i < strings.length; i++) {
15
+ raw += strings[i]
16
+ if (i < values.length) {
17
+ const id = slots.length
18
+ slots.push(values[i])
19
+ raw += SENTINEL + id + SENTINEL
20
+ }
21
+ }
22
+
23
+ const ast = parse(raw, slots)
24
+ if (!ast) throw new Error('Empty template')
25
+ return build(ast, slots)
26
+ }
27
+
28
+ // ---- parser ----
29
+
30
+ function parse(input, slots) {
31
+ let pos = 0
32
+
33
+ function peek() {
34
+ return input[pos]
35
+ }
36
+ function advance() {
37
+ return input[pos++]
38
+ }
39
+ function eof() {
40
+ return pos >= input.length
41
+ }
42
+
43
+ function skip() {
44
+ while (!eof() && /\s/.test(peek())) advance()
45
+ }
46
+
47
+ const SLOT_RE = new RegExp(SENTINEL + '(\\d+)' + SENTINEL, 'g')
48
+ const SLOT_PURE = new RegExp('^' + SENTINEL + '(\\d+)' + SENTINEL + '$')
49
+
50
+ function resolveValue(str) {
51
+ str = str.trim()
52
+ // Pure placeholder — preserve type
53
+ const m = str.match(SLOT_PURE)
54
+ if (m) return slots[parseInt(m[1])]
55
+ // Mixed content — inline replace as strings
56
+ if (str.includes(SENTINEL)) {
57
+ return str.replace(SLOT_RE, (_, id) => String(slots[parseInt(id)]))
58
+ }
59
+ if (str === 'true') return true
60
+ if (str === 'false') return false
61
+ if (str !== '' && !isNaN(str)) return Number(str)
62
+ if (
63
+ (str[0] === '"' && str[str.length - 1] === '"') ||
64
+ (str[0] === "'" && str[str.length - 1] === "'")
65
+ ) {
66
+ return str.slice(1, -1)
67
+ }
68
+ return str
69
+ }
70
+
71
+ function parseNode() {
72
+ skip()
73
+ if (eof()) return null
74
+ if (peek() === '<') {
75
+ if (input[pos + 1] === '/') return null
76
+ return parseElement()
77
+ }
78
+ return parseText()
79
+ }
80
+
81
+ function parseText() {
82
+ let buf = ''
83
+ while (!eof() && peek() !== '<') buf += advance()
84
+ buf = buf.trim()
85
+ if (!buf) return null
86
+ return { type: 'text', value: resolveValue(buf) }
87
+ }
88
+
89
+ function parseElement() {
90
+ advance() // <
91
+
92
+ let name = ''
93
+ while (!eof() && /[a-zA-Z0-9_.]/.test(peek())) name += advance()
94
+
95
+ const attrs = parseAttrs()
96
+
97
+ let selfClose = false
98
+ if (peek() === '/') {
99
+ selfClose = true
100
+ advance()
101
+ }
102
+ advance() // >
103
+
104
+ if (selfClose) return { type: 'element', tag: name, attrs, children: [] }
105
+
106
+ const children = []
107
+ while (!eof()) {
108
+ skip()
109
+ if (eof()) break
110
+ if (peek() === '<' && input[pos + 1] === '/') break
111
+ const child = parseNode()
112
+ if (child) children.push(child)
113
+ else break
114
+ }
115
+
116
+ if (!eof() && peek() === '<' && input[pos + 1] === '/') {
117
+ advance()
118
+ advance()
119
+ while (!eof() && peek() !== '>') advance()
120
+ if (!eof()) advance()
121
+ }
122
+
123
+ return { type: 'element', tag: name, attrs, children }
124
+ }
125
+
126
+ function parseAttrs() {
127
+ const attrs = {}
128
+
129
+ while (!eof()) {
130
+ skip()
131
+ if (peek() === '>' || peek() === '/') break
132
+
133
+ let name = ''
134
+ while (!eof() && /[a-zA-Z0-9_\-]/.test(peek())) name += advance()
135
+ if (!name) break
136
+
137
+ if (peek() === '=') {
138
+ advance()
139
+ let val = ''
140
+
141
+ if (peek() === '"' || peek() === "'") {
142
+ const q = advance()
143
+ while (!eof() && peek() !== q) val += advance()
144
+ if (!eof()) advance()
145
+ } else {
146
+ while (!eof() && !/[\s>\/]/.test(peek())) val += advance()
147
+ }
148
+
149
+ attrs[name] = resolveValue(val)
150
+ } else {
151
+ attrs[name] = true
152
+ }
153
+ }
154
+
155
+ return attrs
156
+ }
157
+
158
+ return parseNode()
159
+ }
160
+
161
+ // ---- builder ----
162
+
163
+ function build(node, slots) {
164
+ if (node.type === 'text') return node.value
165
+
166
+ const Ctor = registry[node.tag]
167
+ if (!Ctor) throw new Error('Unknown cell: ' + node.tag)
168
+
169
+ let style = null
170
+ const children = node.children
171
+ .map((c) => build(c, slots))
172
+ .filter((c) => {
173
+ if (c instanceof Style) {
174
+ style = c
175
+ return false
176
+ }
177
+ return c !== null
178
+ })
179
+
180
+ return new Ctor({
181
+ ...node.attrs,
182
+ children,
183
+ style,
184
+ cells: node.tag.startsWith('Style') ? registry : undefined
185
+ })
186
+ }
187
+
188
+ module.exports = { cellery, register }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cellery",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "cellery",
5
5
  "exports": {
6
6
  "./package": "./package.json",
@@ -24,8 +24,8 @@
24
24
  },
25
25
  "scripts": {
26
26
  "format": "prettier . --write",
27
- "test": "prettier . --check && lunte && brittle-bare test/all.js",
28
- "lint": "lunte"
27
+ "test": "brittle-bare test/all.js",
28
+ "lint": "prettier . --check && lunte"
29
29
  },
30
30
  "repository": {
31
31
  "type": "git",
@@ -38,6 +38,7 @@
38
38
  },
39
39
  "homepage": "https://github.com/holepunchto/cellery",
40
40
  "dependencies": {
41
+ "css-tree": "^3.2.1",
41
42
  "iambus": "^2.0.6"
42
43
  }
43
44
  }