turtb 0.5.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/LICENSE +661 -0
- package/bin/turtb.js +259 -0
- package/lib/display/h.js +204 -0
- package/lib/display/helpers.js +78 -0
- package/lib/display/render.js +177 -0
- package/lib/display/shapes.js +91 -0
- package/lib/turtle/Signer.js +120 -0
- package/lib/turtle/Signer.test.js +38 -0
- package/lib/turtle/TurtleBranch.js +147 -0
- package/lib/turtle/TurtleBranch.test.js +89 -0
- package/lib/turtle/TurtleDictionary.js +157 -0
- package/lib/turtle/TurtleDictionary.test.js +331 -0
- package/lib/turtle/U8aTurtle.js +203 -0
- package/lib/turtle/U8aTurtle.test.js +60 -0
- package/lib/turtle/Workspace.js +62 -0
- package/lib/turtle/Workspace.test.js +63 -0
- package/lib/turtle/codecs/CodecType.js +36 -0
- package/lib/turtle/codecs/CodecTypeVersion.js +37 -0
- package/lib/turtle/codecs/Commit.js +10 -0
- package/lib/turtle/codecs/CompositeCodec.js +86 -0
- package/lib/turtle/codecs/TreeNode.js +38 -0
- package/lib/turtle/codecs/codec.js +441 -0
- package/lib/turtle/connections/AbstractUpdater.js +176 -0
- package/lib/turtle/connections/TurtleBranchMultiplexer.js +102 -0
- package/lib/turtle/connections/TurtleBranchMultiplexer.test.js +26 -0
- package/lib/turtle/connections/TurtleBranchUpdater.js +47 -0
- package/lib/turtle/connections/TurtleDB.js +165 -0
- package/lib/turtle/connections/TurtleDB.test.js +45 -0
- package/lib/turtle/connections/TurtleTalker.js +34 -0
- package/lib/turtle/connections/TurtleTalker.test.js +101 -0
- package/lib/turtle/utils.js +192 -0
- package/lib/turtle/utils.test.js +158 -0
- package/lib/utils/Assert.js +115 -0
- package/lib/utils/NestedSet.js +68 -0
- package/lib/utils/NestedSet.test.js +30 -0
- package/lib/utils/OWN_KEYS.js +1 -0
- package/lib/utils/Recaller.js +175 -0
- package/lib/utils/Recaller.test.js +75 -0
- package/lib/utils/TestRunner.js +200 -0
- package/lib/utils/TestRunner.test.js +144 -0
- package/lib/utils/TestRunnerConstants.js +13 -0
- package/lib/utils/combineUint8ArrayLikes.js +18 -0
- package/lib/utils/combineUint8Arrays.js +23 -0
- package/lib/utils/components.js +88 -0
- package/lib/utils/crypto.js +17 -0
- package/lib/utils/deepEqual.js +16 -0
- package/lib/utils/deepEqual.test.js +27 -0
- package/lib/utils/fileTransformer.js +16 -0
- package/lib/utils/handleRedirect.js +93 -0
- package/lib/utils/logger.js +24 -0
- package/lib/utils/nextTick.js +47 -0
- package/lib/utils/noble-secp256k1.js +602 -0
- package/lib/utils/proxyWithRecaller.js +51 -0
- package/lib/utils/toCombinedVersion.js +14 -0
- package/lib/utils/toSubVersions.js +14 -0
- package/lib/utils/toVersionCount.js +5 -0
- package/lib/utils/webSocketMuxFactory.js +123 -0
- package/lib/utils/zabacaba.js +25 -0
- package/package.json +24 -0
- package/src/ArchiveUpdater.js +99 -0
- package/src/S3Updater.js +99 -0
- package/src/archiveSync.js +28 -0
- package/src/fileSync.js +155 -0
- package/src/getExistenceLength.js +19 -0
- package/src/manageCert.js +36 -0
- package/src/originSync.js +75 -0
- package/src/outletSync.js +50 -0
- package/src/proxyFolder.js +195 -0
- package/src/s3Sync.js +32 -0
- package/src/webSync.js +101 -0
package/bin/turtb.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'fs'
|
|
4
|
+
import { start } from 'repl'
|
|
5
|
+
import { Option, program } from 'commander'
|
|
6
|
+
import { question, questionNewPassword } from 'readline-sync'
|
|
7
|
+
import { LOG_LEVELS, logError, logInfo, logSilly, setLogLevel } from '../lib/utils/logger.js'
|
|
8
|
+
import { Signer } from '../lib/turtle/Signer.js'
|
|
9
|
+
import { TurtleDB } from '../lib/turtle/connections/TurtleDB.js'
|
|
10
|
+
import { Recaller } from '../lib/utils/Recaller.js'
|
|
11
|
+
import { OURS, THEIRS, THROW, TurtleDictionary } from '../lib/turtle/TurtleDictionary.js'
|
|
12
|
+
import { Workspace } from '../lib/turtle/Workspace.js'
|
|
13
|
+
import { AS_REFS } from '../lib/turtle/codecs/CodecType.js'
|
|
14
|
+
import { archiveSync } from '../src/archiveSync.js'
|
|
15
|
+
import { fileSync } from '../src/fileSync.js'
|
|
16
|
+
import { s3Sync } from '../src/s3Sync.js'
|
|
17
|
+
import { originSync } from '../src/originSync.js'
|
|
18
|
+
import { outletSync } from '../src/outletSync.js'
|
|
19
|
+
import { webSync } from '../src/webSync.js'
|
|
20
|
+
import { config } from 'dotenv'
|
|
21
|
+
|
|
22
|
+
console.log('\n\n\n!!!!!')
|
|
23
|
+
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url)))
|
|
24
|
+
|
|
25
|
+
const defaultWebPort = 8080
|
|
26
|
+
const defaultRemoteHost = 'turtledb.com'
|
|
27
|
+
const defaultRemotePort = 1024
|
|
28
|
+
const defaultSyncPort = 1024
|
|
29
|
+
|
|
30
|
+
const makeParserWithOptions = (...options) => value => {
|
|
31
|
+
if (options.length) {
|
|
32
|
+
if (value === true) return options[0]
|
|
33
|
+
if (value === '') return options[0]
|
|
34
|
+
if (value === 'true') return options[0]
|
|
35
|
+
}
|
|
36
|
+
if (value === 'false') return false
|
|
37
|
+
if (!isNaN(+value)) value = +value
|
|
38
|
+
if (options.length <= 1 || options.includes(value)) return value
|
|
39
|
+
throw new Error(`value must be one of: ${options.join(', ')}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
program
|
|
43
|
+
.name('turtledb-com')
|
|
44
|
+
.version(version)
|
|
45
|
+
|
|
46
|
+
.option('--env-file <path>', 'path to .env file')
|
|
47
|
+
|
|
48
|
+
.addOption(
|
|
49
|
+
new Option('--username <string>', 'username to use for Signer')
|
|
50
|
+
.env('TURTLEDB_USERNAME')
|
|
51
|
+
)
|
|
52
|
+
.addOption(
|
|
53
|
+
new Option('--password <string>', 'password to use for Signer')
|
|
54
|
+
.env('TURTLEDB_PASSWORD')
|
|
55
|
+
)
|
|
56
|
+
.addOption(
|
|
57
|
+
new Option('--turtlename <string>', 'name for dataset')
|
|
58
|
+
.env('TURTLEDB_TURTLENAME')
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
.addOption(
|
|
62
|
+
new Option('-f, --fs-mirror [resolve]', 'mirror files locally and handle')
|
|
63
|
+
.default(false)
|
|
64
|
+
.preset(THROW)
|
|
65
|
+
.choices([OURS, THEIRS, THROW, ''])
|
|
66
|
+
.argParser(makeParserWithOptions(THROW, OURS, THEIRS))
|
|
67
|
+
.env('TURTLEDB_FS_MIRROR')
|
|
68
|
+
)
|
|
69
|
+
.addOption(
|
|
70
|
+
new Option('-i, --interactive', 'flag to start repl')
|
|
71
|
+
.default(false)
|
|
72
|
+
.env('TURTLEDB_INTERACTIVE')
|
|
73
|
+
)
|
|
74
|
+
.addOption(
|
|
75
|
+
new Option('-a, --archive', 'save all turtles to files by public key')
|
|
76
|
+
.default(false)
|
|
77
|
+
.env('TURTLEDB_ARCHIVE')
|
|
78
|
+
)
|
|
79
|
+
.addOption(
|
|
80
|
+
new Option('-v, --verbose [level]', 'log data flows')
|
|
81
|
+
.default(0)
|
|
82
|
+
.preset(1)
|
|
83
|
+
.choices(Object.values(LOG_LEVELS).map(v => v.toString()))
|
|
84
|
+
.argParser(makeParserWithOptions(1, ...Object.values(LOG_LEVELS)))
|
|
85
|
+
.env('TURTLEDB_VERBOSE')
|
|
86
|
+
)
|
|
87
|
+
.addOption(
|
|
88
|
+
new Option('--turtleDB-folder <path>', 'path to folder for TurtleDB files')
|
|
89
|
+
.default('.turtleDB')
|
|
90
|
+
.env('TURTLEDB_FOLDER')
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
.addOption(
|
|
94
|
+
new Option('-w, --web-port [number]', 'web port to sync from')
|
|
95
|
+
.default(false)
|
|
96
|
+
.preset(defaultWebPort)
|
|
97
|
+
.argParser(makeParserWithOptions(defaultWebPort))
|
|
98
|
+
.env('TURTLEDB_WEB_PORT')
|
|
99
|
+
.helpGroup('Web Server:')
|
|
100
|
+
)
|
|
101
|
+
.addOption(
|
|
102
|
+
new Option('--web-fallback <string>', 'project public key to use as fallback for web')
|
|
103
|
+
.env('TURTLEDB_WEB_FALLBACK')
|
|
104
|
+
.helpGroup('Web Server:')
|
|
105
|
+
)
|
|
106
|
+
.addOption(
|
|
107
|
+
new Option('--web-certpath <string>', 'path to self-cert for web')
|
|
108
|
+
.env('TURTLEDB_WEB_CERTPATH')
|
|
109
|
+
.helpGroup('Web Server:')
|
|
110
|
+
)
|
|
111
|
+
.addOption(
|
|
112
|
+
new Option('--web-insecure', '(local dev) allow unauthorized for web')
|
|
113
|
+
.env('TURTLEDB_WEB_INSECURE')
|
|
114
|
+
.helpGroup('Web Server:')
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
.addOption(
|
|
118
|
+
new Option('--remote-host [string]', 'remote host to sync to')
|
|
119
|
+
.default(false)
|
|
120
|
+
.preset(defaultRemoteHost)
|
|
121
|
+
.argParser(makeParserWithOptions(defaultRemoteHost))
|
|
122
|
+
.env('TURTLEDB_REMOTE_HOST')
|
|
123
|
+
.helpGroup('TurtleDB Syncing:')
|
|
124
|
+
)
|
|
125
|
+
.addOption(
|
|
126
|
+
new Option('-r, --remote-port [number]', 'remote port to sync to')
|
|
127
|
+
.default(false)
|
|
128
|
+
.preset(defaultRemotePort)
|
|
129
|
+
.argParser(makeParserWithOptions(defaultRemotePort))
|
|
130
|
+
.env('TURTLEDB_REMOTE_PORT')
|
|
131
|
+
.helpGroup('TurtleDB Syncing:')
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
.addOption(
|
|
135
|
+
new Option('-p, --sync-port [number]', 'local port to sync from')
|
|
136
|
+
.default(false)
|
|
137
|
+
.preset(defaultSyncPort)
|
|
138
|
+
.argParser(makeParserWithOptions(defaultSyncPort))
|
|
139
|
+
.env('TURTLEDB_PORT')
|
|
140
|
+
.helpGroup('TurtleDB Syncing:')
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
.addOption(
|
|
144
|
+
new Option('--s3-end-point <string>', 'endpoint for s3 (like "https://sfo3.digitaloceanspaces.com")')
|
|
145
|
+
.default(false)
|
|
146
|
+
.argParser(makeParserWithOptions())
|
|
147
|
+
.env('TURTLEDB_S3_END_POINT')
|
|
148
|
+
.helpGroup('S3-like Service Syncing:')
|
|
149
|
+
)
|
|
150
|
+
.addOption(
|
|
151
|
+
new Option('--s3-region <string>', 'region for s3 (like "sfo3")')
|
|
152
|
+
.env('TURTLEDB_S3_REGION')
|
|
153
|
+
.helpGroup('S3-like Service Syncing:')
|
|
154
|
+
)
|
|
155
|
+
.addOption(
|
|
156
|
+
new Option('--s3-access-key-id <string>', 'accessKeyId for s3')
|
|
157
|
+
.env('TURTLEDB_S3_ACCESS_KEY_ID')
|
|
158
|
+
.helpGroup('S3-like Service Syncing:')
|
|
159
|
+
)
|
|
160
|
+
.addOption(
|
|
161
|
+
new Option('--s3-secret-access-key <string>', 'secretAccessKey for s3')
|
|
162
|
+
.env('TURTLEDB_S3_SECRET_ACCESS_KEY')
|
|
163
|
+
.helpGroup('S3-like Service Syncing:')
|
|
164
|
+
)
|
|
165
|
+
.addOption(
|
|
166
|
+
new Option('--s3-bucket <string>', 'bucket for s3')
|
|
167
|
+
.env('TURTLEDB_S3_BUCKET')
|
|
168
|
+
.helpGroup('S3-like Service Syncing:')
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
.parse()
|
|
172
|
+
|
|
173
|
+
const options = program.opts()
|
|
174
|
+
if (options.envFile) {
|
|
175
|
+
config({ path: options.envFile })
|
|
176
|
+
program.parse() // re-parse with new env vars
|
|
177
|
+
Object.assign(options, program.opts()) // update options with new env vars
|
|
178
|
+
}
|
|
179
|
+
setLogLevel(options.verbose)
|
|
180
|
+
let username = options.username
|
|
181
|
+
let turtlename = options.turtlename
|
|
182
|
+
let signer
|
|
183
|
+
if (options.fsMirror !== false) {
|
|
184
|
+
username ||= question('Username: ')
|
|
185
|
+
turtlename ||= question('Turtlename: ')
|
|
186
|
+
signer = new Signer(username, options.password || questionNewPassword('Password [ATTENTION!: Backspace won\'t work here]: ', { min: 4, max: 999 }))
|
|
187
|
+
} else if (username && turtlename && options.password) {
|
|
188
|
+
signer = new Signer(username, options.password)
|
|
189
|
+
}
|
|
190
|
+
const publicKey = (await signer.makeKeysFor(turtlename)).publicKey
|
|
191
|
+
logInfo(() => console.log({ username, turtlename, publicKey }))
|
|
192
|
+
|
|
193
|
+
logSilly(() => console.log({ options }))
|
|
194
|
+
// console.log({ options })
|
|
195
|
+
// process.exit(0)
|
|
196
|
+
|
|
197
|
+
const recaller = new Recaller('turtledb-com')
|
|
198
|
+
const turtleDB = new TurtleDB('turtledb-com', recaller)
|
|
199
|
+
|
|
200
|
+
if (options.syncPort !== false) {
|
|
201
|
+
const syncPort = +options.syncPort || defaultSyncPort
|
|
202
|
+
logInfo(() => console.log(`listening for local connections on port ${syncPort}`))
|
|
203
|
+
outletSync(turtleDB, syncPort)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (options.remoteHost !== false || options.remotePort !== false) {
|
|
207
|
+
const remoteHost = options.remoteHost || defaultRemoteHost
|
|
208
|
+
const remotePort = +options.remotePort || defaultRemotePort
|
|
209
|
+
logInfo(() => console.log(`connecting to remote at ${remoteHost}:${remotePort}`))
|
|
210
|
+
originSync(turtleDB, remoteHost, remotePort)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (options.s3EndPoint !== false) {
|
|
214
|
+
s3Sync(turtleDB, recaller, options.s3EndPoint, options.s3Region, options.s3AccessKeyId, options.s3SecretAccessKey, options.s3Bucket)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (options.archive) {
|
|
218
|
+
const archivePath = `${options.turtleDBFolder}/archive`
|
|
219
|
+
logInfo(() => console.log(`archiving to ${archivePath}`))
|
|
220
|
+
archiveSync(turtleDB, recaller, archivePath)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (options.fsMirror !== false) {
|
|
224
|
+
if (![OURS, THEIRS, THROW].includes(options.fsMirror)) {
|
|
225
|
+
logError(() => console.error(`fs-mirror resolve option must be "${OURS}", "${THEIRS}" or "${THROW}" (you provided: "${options.fsMirror}")`))
|
|
226
|
+
process.exit(1)
|
|
227
|
+
}
|
|
228
|
+
logInfo(() => console.log('mirroring to file system'))
|
|
229
|
+
fileSync(turtlename, turtleDB, signer, undefined, options.fsMirror, options.turtleDBFolder)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (options.webPort !== false) {
|
|
233
|
+
const webPort = +options.webPort
|
|
234
|
+
const insecure = !!options.webInsecure
|
|
235
|
+
const https = insecure || !!options.webCertpath
|
|
236
|
+
const certpath = options.webCertpath || `${options.turtleDBFolder}/dev/cert.json`
|
|
237
|
+
logInfo(() => console.log(`listening for web connections on port ${webPort} (https: ${https}, insecure: ${insecure}, certpath: ${certpath})`))
|
|
238
|
+
webSync(webPort, publicKey, turtleDB, https, insecure, certpath, options.turtleDBFolder)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (options.interactive) {
|
|
242
|
+
global.username = username
|
|
243
|
+
global.turtlename = turtlename
|
|
244
|
+
global.signer = signer
|
|
245
|
+
global.publicKey = publicKey
|
|
246
|
+
global.recaller = recaller
|
|
247
|
+
global.turtleDB = turtleDB
|
|
248
|
+
global.workspace = await turtleDB.makeWorkspace(signer, turtlename)
|
|
249
|
+
global.TurtleDictionary = TurtleDictionary
|
|
250
|
+
global.Signer = Signer
|
|
251
|
+
global.Workspace = Workspace
|
|
252
|
+
global.setLogLevel = setLogLevel
|
|
253
|
+
global.AS_REFS = AS_REFS
|
|
254
|
+
const replServer = start({ breakEvalOnSigint: true })
|
|
255
|
+
replServer.setupHistory('.node_repl_history', err => {
|
|
256
|
+
if (err) logError(() => console.error(err))
|
|
257
|
+
})
|
|
258
|
+
replServer.on('exit', process.exit)
|
|
259
|
+
}
|
package/lib/display/h.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const _voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
|
|
2
|
+
const END = Symbol('end')
|
|
3
|
+
|
|
4
|
+
function _assertChar (arr, regex) {
|
|
5
|
+
if (!arr[arr.i].match(regex)) {
|
|
6
|
+
throw new Error(`expected ${regex}. got ${arr[arr.i]} at i=${arr.i}`)
|
|
7
|
+
}
|
|
8
|
+
arr.i++
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function _readValue (arr) {
|
|
12
|
+
if (arr[arr.i].isValue) {
|
|
13
|
+
return arr[arr.i++]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function _readEscaped (arr) {
|
|
18
|
+
_assertChar(arr, /&/)
|
|
19
|
+
if (_readIf(arr, 'amp;')) {
|
|
20
|
+
return '&'
|
|
21
|
+
} else if (_readIf(arr, 'apos;')) {
|
|
22
|
+
return '\''
|
|
23
|
+
} else if (_readIf(arr, 'gt;')) {
|
|
24
|
+
return '>'
|
|
25
|
+
} else if (_readIf(arr, 'lt;')) {
|
|
26
|
+
return '<'
|
|
27
|
+
} else if (_readIf(arr, 'quot;')) {
|
|
28
|
+
return '"'
|
|
29
|
+
} else if (_readIf(arr, 'nbsp;')) {
|
|
30
|
+
return ' '
|
|
31
|
+
} else {
|
|
32
|
+
throw new Error('unhandled escape sequence')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _readTo (arr, regex) {
|
|
37
|
+
const ss = []
|
|
38
|
+
while (arr.i < arr.length) {
|
|
39
|
+
const c = arr[arr.i]
|
|
40
|
+
if (c.isValue || c.match(regex)) {
|
|
41
|
+
return ss.join('')
|
|
42
|
+
} else if (c === '&') {
|
|
43
|
+
ss.push(_readEscaped(arr))
|
|
44
|
+
} else {
|
|
45
|
+
ss.push(c)
|
|
46
|
+
arr.i++
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return ss.join('')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _skipWhiteSpace (arr) {
|
|
53
|
+
_readTo(arr, /\S/)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function _readIf (arr, str) {
|
|
57
|
+
if (!str.length) {
|
|
58
|
+
str = [str]
|
|
59
|
+
}
|
|
60
|
+
const out = []
|
|
61
|
+
for (let i = 0; i < str.length; i++) {
|
|
62
|
+
const char = arr[arr.i + i]
|
|
63
|
+
if (!char || !char.match || !char.match(str[i])) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
out.push(char)
|
|
67
|
+
}
|
|
68
|
+
arr.i += str.length
|
|
69
|
+
return out.join('')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _readValueParts (arr, regex) {
|
|
73
|
+
const out = []
|
|
74
|
+
let ss = []
|
|
75
|
+
while (arr.i < arr.length) {
|
|
76
|
+
const c = arr[arr.i]
|
|
77
|
+
if (c.isValue) {
|
|
78
|
+
if (ss.length) {
|
|
79
|
+
out.push({ type: 'part', value: ss.join('') })
|
|
80
|
+
ss = []
|
|
81
|
+
}
|
|
82
|
+
out.push(c.value)
|
|
83
|
+
arr.i++
|
|
84
|
+
} else if (c.match(regex)) {
|
|
85
|
+
if (ss.length) {
|
|
86
|
+
out.push({ type: 'part', value: ss.join('') })
|
|
87
|
+
}
|
|
88
|
+
return out
|
|
89
|
+
} else if (c === '&') {
|
|
90
|
+
ss.push(_readEscaped(arr))
|
|
91
|
+
} else {
|
|
92
|
+
ss.push(c)
|
|
93
|
+
arr.i++
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _decodeAttribute (arr) {
|
|
99
|
+
_skipWhiteSpace(arr)
|
|
100
|
+
const c = arr[arr.i]
|
|
101
|
+
if (c === '/' || c === '>') {
|
|
102
|
+
return END
|
|
103
|
+
}
|
|
104
|
+
let name = _readValue(arr)
|
|
105
|
+
if (name && name.isValue) {
|
|
106
|
+
return name.value
|
|
107
|
+
}
|
|
108
|
+
name = _readTo(arr, /[\s=/>]/)
|
|
109
|
+
if (!name) {
|
|
110
|
+
throw new Error('attribute must have a name (dynamic attributes okay, dynamic names... sorry)')
|
|
111
|
+
}
|
|
112
|
+
_skipWhiteSpace(arr)
|
|
113
|
+
const equalSign = _readIf(arr, '=')
|
|
114
|
+
if (equalSign) {
|
|
115
|
+
_skipWhiteSpace(arr)
|
|
116
|
+
let value = _readValue(arr)
|
|
117
|
+
if (value) {
|
|
118
|
+
value = value.value
|
|
119
|
+
} else {
|
|
120
|
+
const quote = _readIf(arr, /['"]/)
|
|
121
|
+
if (quote) {
|
|
122
|
+
value = _readValueParts(arr, quote)
|
|
123
|
+
_assertChar(arr, quote)
|
|
124
|
+
} else {
|
|
125
|
+
value = _readTo(arr, /[\s=/>]/)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { type: 'attribute', name, value }
|
|
129
|
+
} else {
|
|
130
|
+
return { type: 'attribute', name }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function _decodeAttributes (arr) {
|
|
135
|
+
const attributes = []
|
|
136
|
+
while (true) {
|
|
137
|
+
const attribute = _decodeAttribute(arr)
|
|
138
|
+
if (attribute !== END) {
|
|
139
|
+
attributes.push(attribute)
|
|
140
|
+
} else {
|
|
141
|
+
return attributes
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _decodeTag (arr) {
|
|
147
|
+
_skipWhiteSpace(arr)
|
|
148
|
+
const c = arr[arr.i]
|
|
149
|
+
if (c.isValue) {
|
|
150
|
+
arr.i++
|
|
151
|
+
return c.value
|
|
152
|
+
}
|
|
153
|
+
return _readTo(arr, /[\s/>]/)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function _decodeElement (arr) {
|
|
157
|
+
const c = arr[arr.i]
|
|
158
|
+
if (c.isValue) {
|
|
159
|
+
arr.i++
|
|
160
|
+
return c.value
|
|
161
|
+
} else if (c === '<') {
|
|
162
|
+
_assertChar(arr, /</)
|
|
163
|
+
const isClosing = _readIf(arr, '/')
|
|
164
|
+
const tag = _decodeTag(arr)
|
|
165
|
+
const isVoid = _voidElements.has(tag)
|
|
166
|
+
const attributes = _decodeAttributes(arr)
|
|
167
|
+
const isEmpty = _readIf(arr, '/') || isVoid
|
|
168
|
+
_assertChar(arr, />/)
|
|
169
|
+
const children = (isClosing || isEmpty) ? [] : _decodeElements(arr, tag)
|
|
170
|
+
if (isVoid && isClosing) return null
|
|
171
|
+
return { type: 'node', tag, attributes, children, isClosing }
|
|
172
|
+
} else {
|
|
173
|
+
return { type: 'textnode', value: _readTo(arr, /</) }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function _decodeElements (arr, closingTag) {
|
|
178
|
+
const nodes = []
|
|
179
|
+
while (arr.i < arr.length) {
|
|
180
|
+
const node = _decodeElement(arr)
|
|
181
|
+
if (node != null) {
|
|
182
|
+
if (node.isClosing) {
|
|
183
|
+
if (closingTag != null) {
|
|
184
|
+
return nodes
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
delete node.isClosing
|
|
188
|
+
nodes.push(node)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return [].concat.apply([], nodes)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function h (strings, ...values) {
|
|
196
|
+
const ss = [strings[0].split('')]
|
|
197
|
+
for (let i = 0; i < values.length; i++) {
|
|
198
|
+
ss.push({ value: values[i], isValue: true })
|
|
199
|
+
ss.push(strings[i + 1].split(''))
|
|
200
|
+
}
|
|
201
|
+
const arr = [].concat.apply([], ss)
|
|
202
|
+
arr.i = 0
|
|
203
|
+
return _decodeElements(arr, null)
|
|
204
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// takes a function that evaluates to true or false and static if and else responses. return function that evaluates condition and returns appropriate response
|
|
2
|
+
export function showIfElse (condition, a, b = []) {
|
|
3
|
+
return () => condition() ? a : b
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// takes an object and a mapping-function. returns a function that maps the object through the function and returns previously calculated mappings on future calls
|
|
7
|
+
export function mapEntries (object, f) {
|
|
8
|
+
let cache = {}
|
|
9
|
+
return () => {
|
|
10
|
+
const o = (typeof object === 'function' ? object() : object) || {}
|
|
11
|
+
const newCache = {}
|
|
12
|
+
const mappedEntries = Object.entries(o).map(([name, input]) => {
|
|
13
|
+
if (cache[name] && cache[name].input === input) {
|
|
14
|
+
newCache[name] = cache[name]
|
|
15
|
+
} else {
|
|
16
|
+
newCache[name] = {
|
|
17
|
+
input,
|
|
18
|
+
output: f(input, name)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return newCache[name].output
|
|
22
|
+
})
|
|
23
|
+
cache = newCache
|
|
24
|
+
return mappedEntries
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// TODO: this only works for unique outputs... fix that
|
|
29
|
+
/*
|
|
30
|
+
export function looselyMapEntries(object, f) {
|
|
31
|
+
const map = new Map()
|
|
32
|
+
return () => {
|
|
33
|
+
const o = (typeof object === 'function' ? object() : object) || {}
|
|
34
|
+
map.forEach(indexedValues => {
|
|
35
|
+
indexedValues.index = 0
|
|
36
|
+
})
|
|
37
|
+
const mappedEntries = Object.entries(o).map(([name, value]) => {
|
|
38
|
+
if (!map.has(value)) {
|
|
39
|
+
map.set(value, { index: 0, values: [] })
|
|
40
|
+
}
|
|
41
|
+
const indexedValues = map.get(value)
|
|
42
|
+
if (indexedValues.values.length <= indexedValues.index) {
|
|
43
|
+
indexedValues.values.push(f(value, name))
|
|
44
|
+
}
|
|
45
|
+
return indexedValues.values[indexedValues.index++]
|
|
46
|
+
})
|
|
47
|
+
// cleanup any unmapped-to values
|
|
48
|
+
map.forEach((indexedValues, key) => {
|
|
49
|
+
if (indexedValues.index) {
|
|
50
|
+
indexedValues.values.splice(indexedValues.index)
|
|
51
|
+
} else {
|
|
52
|
+
map.delete(key)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
return mappedEntries
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
export function mapSwitch (expression, f) {
|
|
61
|
+
const map = new Map()
|
|
62
|
+
return () => {
|
|
63
|
+
const expr = expression()
|
|
64
|
+
if (!map.has(expr)) {
|
|
65
|
+
map.set(expr, f(expr))
|
|
66
|
+
}
|
|
67
|
+
return map.get(expr)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function objToDeclarations (obj = {}) {
|
|
72
|
+
return Object.entries(obj).map(([name, value]) => `${name}: ${value};`).join('')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// people just don't like writing 'el => e => {...}'
|
|
76
|
+
export function handle (handler, ...args) {
|
|
77
|
+
return el => e => handler(e, el, ...args)
|
|
78
|
+
}
|