votive 0.0.1 → 0.0.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 ADDED
@@ -0,0 +1,3 @@
1
+ # Votive
2
+
3
+ TK
package/jsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "checkJs": true,
5
+ "noImplicitlyAny": true,
6
+ "module": "esnext",
7
+ "target": "esnext",
8
+ "moduleResolution": "nodenext"
9
+ }
10
+ }
package/lib/bundle.js ADDED
@@ -0,0 +1,178 @@
1
+ import { default as createDatabase } from "./createDatabase.js"
2
+ import readSources from "./readSources.js"
3
+ import readAbstracts from "./readAbstracts.js"
4
+ import readFolders from "./readFolders.js"
5
+ import writeDestinations from "./writeDestinations.js"
6
+ import runJobs from "./runJobs.js"
7
+
8
+ /** @import {Database} from "./createDatabase.js" */
9
+
10
+ /**
11
+ * @typedef {object} VotivePlugin
12
+ * @property {string} name
13
+ * @property {VotiveProcessor[]} [processors]
14
+ * @property {Record<string, Runner>} [runners]
15
+ * @property {Router} [router]
16
+ */
17
+
18
+ /**
19
+ * @typedef {object} VotiveProcessor
20
+ * @property {ProcessorFilter} [filter]
21
+ * @property {ProcessorSyntax} syntax
22
+ * @property {ProcessorRead} [read]
23
+ * @property {ProcessorWrite} [write]
24
+ */
25
+
26
+ /**
27
+ * Filter for files that the processor will read from.
28
+ * @typedef {object} ProcessorFilter
29
+ * @property {string[]} extensions
30
+ */
31
+
32
+ /**
33
+ * A name for the syntax that processors will read-to and write-from (e.g. Unified.js's "hast").
34
+ * @typedef {string} ProcessorSyntax
35
+ */
36
+
37
+ /**
38
+ * @typedef {object} ProcessorRead
39
+ * @property {ReadPath} [path]
40
+ * @property {ReadText} [text]
41
+ * @property {ReadAbstract} [abstract]
42
+ * @property {ReadFolder} [folder]
43
+ */
44
+
45
+ /**
46
+ * Read a file path and return any necessary jobs.
47
+ * @callback ReadPath
48
+ * @param {string} filePath
49
+ * @param {Database} database
50
+ * @param {VotiveConfig} config
51
+ * @returns {Job[] | undefined}
52
+ */
53
+
54
+ /**
55
+ * @callback ReadText
56
+ * @param {string} text
57
+ * @param {Database} database
58
+ * @param {VotiveConfig} config
59
+ * @returns {ReadTextResult | undefined}
60
+ */
61
+
62
+ /**
63
+ * @typedef {object} ReadTextResult
64
+ * @property {Jobs} [jobs]
65
+ * @property {object} [metadata]
66
+ * @property {Abstract} [abstract]
67
+ */
68
+
69
+ /**
70
+ * @typedef {Job[]} Jobs
71
+ */
72
+
73
+ /**
74
+ * @callback ReadAbstract
75
+ * @param {Abstract} abstract
76
+ * @param {Database} database
77
+ * @param {VotiveConfig} config
78
+ * @returns {{abstract: Abstract, jobs: Jobs}}
79
+ */
80
+
81
+ /**
82
+ * @typedef {Record<string, any>} Abstract
83
+ */
84
+
85
+ /**
86
+ * @typedef {Abstract[]} Abstracts
87
+ */
88
+
89
+ /**
90
+ * @typedef {AbstractWithSyntax[]} AbstractsWithSyntax
91
+ */
92
+
93
+ /**
94
+ * @typedef {object} AbstractWithSyntax
95
+ * @property {Abstract} abstract
96
+ * @property {ProcessorSyntax} syntax
97
+ */
98
+
99
+ /**
100
+ * @callback ReadFolder
101
+ * @param {object} Folder
102
+ * @param {Database} database
103
+ * @param {VotiveConfig} config
104
+ */
105
+
106
+ /**
107
+ * @typedef {object} Folder
108
+ * @property {string} path
109
+ */
110
+
111
+ /**
112
+ * Build and write destination files.
113
+ * @callback ProcessorWrite
114
+ * @param {object} destination
115
+ * @param {Database} database
116
+ * @param {VotiveConfig} config
117
+ * @returns {{ data: string, encoding?: BufferEncoding = 'utf-8' }}
118
+ */
119
+
120
+ /**
121
+ * A function that runs a job, suggest as fetching data or formatting an image.
122
+ * @callback Runner
123
+ * @param {string} file
124
+ * @param {Database} database
125
+ * @returns {Promise<any>}
126
+ */
127
+
128
+ /**
129
+ * Run a job.
130
+ * @typedef {object} Job
131
+ * @property {object} data
132
+ * @property {string} runner
133
+ * @property {string} [plugin]
134
+ */
135
+
136
+ /**
137
+ * @callback Router
138
+ * @param {string} path
139
+ * @returns {string | false | undefined}
140
+ */
141
+
142
+ /**
143
+ * @typedef {object} VotiveConfig
144
+ * @property {string} sourceFolder
145
+ * @property {string} destinationFolder
146
+ * @property {VotivePlugin[]} plugins
147
+ */
148
+
149
+ /**
150
+ * @typedef {object} FlatProcessor
151
+ * @property {VotivePlugin} plugin
152
+ * @property {VotiveProcessor} processor
153
+ */
154
+
155
+ /**
156
+ * @typedef {FlatProcessor[]} FlatProcessors
157
+ */
158
+
159
+ /**
160
+ * @param {VotiveConfig} config
161
+ */
162
+ async function bundle(config) {
163
+ const processors = config.plugins
164
+ && config.plugins.flatMap(plugin => plugin.processors && plugin.processors.map(processor => ({ plugin, processor })))
165
+
166
+ const database = createDatabase()
167
+ const { folders, sources } = await readSources(config, database, processors)
168
+
169
+ const sourcesJobs = sources.flatMap(source => source.jobs) || []
170
+ const { processedAbstracts, abstractsJobs } = readAbstracts(sources, config, database, processors)
171
+ const foldersJobs = readFolders(folders, config, database, processors) || []
172
+ await writeDestinations(config, database)
173
+
174
+ const jobs = [...sourcesJobs, ...abstractsJobs, ...foldersJobs]
175
+ await runJobs(jobs, config, database)
176
+ }
177
+
178
+ export default bundle
@@ -0,0 +1,323 @@
1
+ import { DatabaseSync, backup } from "node:sqlite"
2
+ import { splitURL } from "./utils/index.js"
3
+ import { statSync } from "node:fs"
4
+
5
+ /**
6
+ * @typedef {ReturnType<createDatabase>} Database
7
+ */
8
+
9
+
10
+ function checkFile(path) {
11
+ try {
12
+ return statSync(path)
13
+ } catch(e) {
14
+ return null
15
+ }
16
+ }
17
+
18
+
19
+ function createDatabase() {
20
+ const databaseSync = new DatabaseSync(".votive.db")
21
+
22
+ const store = databaseSync.createTagStore()
23
+
24
+ databaseSync.exec(sqlCreateTables)
25
+
26
+ const database = {}
27
+
28
+ /**
29
+ * @param {string} folderPath
30
+ * @param {string} urlPath
31
+ */
32
+ database.createFolder = (folderPath, urlPath) => {
33
+ return store.get`INSERT INTO folders (folderPath, urlPath) VALUES :${folderPath} :${urlPath} RETURNING *`
34
+ }
35
+
36
+ const createSource = databaseSync.prepare(`INSERT INTO sources (path, destination, lastModified) VALUES (?, ?, ?)`)
37
+
38
+ /**
39
+ * @param {string} source - Source file path.
40
+ * @param {string} destination - Destination file path.
41
+ * @param {number} lastModified - Source file date last modified.
42
+ */
43
+ database.createSource = (source, destination, lastModified) => {
44
+ databaseSync.prepare(``) // SQLite bug. Query fails without this.
45
+ const inserted = createSource.get(source, destination, lastModified)
46
+ }
47
+
48
+
49
+ // These statements are cached, no need to refactor
50
+ const createDest = databaseSync.prepare(`INSERT INTO destinations (path, dir, syntax, stale, abstract) VALUES (?, ?, ?, 1, ?)`)
51
+ const updateDest = databaseSync.prepare(`UPDATE destinations SET abstract = ? WHERE path = ?`)
52
+ const createMeta = databaseSync.prepare(`INSERT INTO metadata (destination, label, value, type) VALUES (?, ?, ?, ?)`)
53
+ const updateMeta = databaseSync.prepare(`UPDATE metadata SET value = ? WHERE destination = ? AND label = ?`)
54
+ const getDepends = databaseSync.prepare(`SELECT * FROM dependencies WHERE destination = ? AND property = ?`)
55
+ const staleDepen = databaseSync.prepare(`UPDATE destinations SET stale = 1 WHERE path = ?`)
56
+ const staleDescendents = databaseSync.prepare(`UPDATE destinations SET stale = 1 WHERE path LIKE ? RETURNING *`)
57
+
58
+ /**
59
+ * @param {object} params
60
+ * @param {string} params.path - Destination file path
61
+ * @param {object} params.abstract
62
+ * @param {object} params.metadata
63
+ * @param {string} params.syntax - Destination abstract syntax
64
+ */
65
+ database.createOrUpdateDestination = ({ metadata, ...dest }) => {
66
+ const [dir] = dest.path ? splitURL(dest.path) : []
67
+ const params = Object.keys(metadata)
68
+ if (dest.abstract) params.push("abstract")
69
+ const extant = database.getDestinationIndependently(dest.path, params)
70
+
71
+ if (!extant) {
72
+ createDest.get(dest.path, dir, dest.syntax, JSON.stringify(dest.abstract))
73
+ Object.entries(metadata).map(([k, v]) => createMeta.get(dest.path, k, v, typeof v))
74
+ return
75
+ }
76
+
77
+ const changedAbstract = JSON.stringify(dest.abstract) !== JSON.stringify(extant.abstract)
78
+ const changedMetadata = []
79
+
80
+ for (const key in metadata) {
81
+ if (JSON.stringify(metadata[key]) !== JSON.stringify(extant.metadata[key])) {
82
+ updateMeta.get(metadata[key], dest.path, key)
83
+ const dependencies = getDepends.all(dest.path, key)
84
+ dependencies.forEach(({ dependent }) => {
85
+ staleDepen.get(dependent)
86
+ })
87
+ }
88
+ }
89
+
90
+ if (changedAbstract) {
91
+ const updated = updateDest.get(JSON.stringify(dest.abstract), dest.path)
92
+ const dependencies = getDepends.all(dest.path, "abstract")
93
+ dependencies.forEach(({ dependent }) => {
94
+ staleDepen.get(dependent)
95
+ })
96
+ }
97
+ }
98
+
99
+ /**
100
+ * @param {string} path
101
+ * @param {string[]} params
102
+ */
103
+ database.getDestinationIndependently = (path, params) => {
104
+ // TODO: Could potentially speed up the next two db queries by using the tag store
105
+ const metadatas = databaseSync.prepare(`
106
+ SELECT * FROM destinations d
107
+ LEFT JOIN metadata m ON d.path = m.destination AND m.label IN (${params.map(p => `'${p}'`).join(", ")})
108
+ WHERE d.path = ?
109
+ `).all(path)
110
+
111
+ const [first] = metadatas
112
+
113
+ if (!first) return
114
+
115
+ const metadata = Object.fromEntries(
116
+ metadatas.map(({ label, value, type }) => {
117
+ const originalValue = type === "undefined"
118
+ ? undefined
119
+ : type === "string"
120
+ ? String(value)
121
+ : type === "boolean"
122
+ ? Boolean(value)
123
+ : typeof value !== "string"
124
+ ? value
125
+ : JSON.parse(value)
126
+ return [label, value]
127
+ })
128
+ )
129
+
130
+
131
+ const result = {
132
+ path: first.path,
133
+ dir: first.dir,
134
+ syntax: first.syntax,
135
+ metadata
136
+ }
137
+
138
+ const includeAbstract = params.includes("abstract")
139
+ if (includeAbstract) result.abstract = JSON.parse(first.abstract)
140
+
141
+ return result
142
+ }
143
+
144
+ /**
145
+ * @param {string} path
146
+ * @param {string[]} properties
147
+ * @param {string} dependent
148
+ */
149
+ database.getDestinationDependently = (path, properties, dependent) => {
150
+ const params = []
151
+ const deps = []
152
+
153
+ properties.forEach(p => {
154
+ params.push(p)
155
+ deps.push(`('${path}', '${p}', '${dependent}')`)
156
+ })
157
+
158
+ const result = database.getDestinationIndependently(path, params)
159
+ if (result.abstract) deps.push(`('${path}, 'abstract', '${dependent}')`)
160
+ const createDepp = databaseSync.prepare(`INSERT INTO dependencies (destination, property, dependent) VALUES ${deps.join(", ")}`).all()
161
+ }
162
+
163
+ const selectSource = databaseSync.prepare(`SELECT * FROM sources WHERE path = ?`)
164
+
165
+ /**
166
+ * @param {string} path
167
+ */
168
+ database.getSource = (path) => {
169
+ const source = selectSource.get(path)
170
+ return source
171
+ }
172
+
173
+ database.getAllSources = () => {
174
+ const sources = store.all`SELECT * FROM sources`
175
+ return sources
176
+ }
177
+
178
+ database.getAllDestinations = () => {
179
+ return store.all`SELECT * FROM destinations`
180
+ }
181
+
182
+ database.getStaleDestinations = () => {
183
+ return store.all`SELECT * FROM destinations WHERE stale = 1`
184
+ }
185
+
186
+ const createSetting = databaseSync.prepare(`INSERT INTO settings (destination, label, value) VALUES (?, ?, ?) RETURNING *`)
187
+ const selectSetting = databaseSync.prepare(`SELECT * FROM settings WHERE label = ? AND destination = ?`)
188
+ const updateSetting = databaseSync.prepare(`UPDATE settings SET value = ? WHERE destination = ? AND label = ?`)
189
+
190
+ // const updateDest = databaseSync.prepare(`UPDATE destinations SET abstract = ? WHERE path = ?`)
191
+
192
+ /**
193
+ * @param {string} destination
194
+ * @param {string} key
195
+ * @param {string} value
196
+ */
197
+ database.setSetting = (destination, key, value) => {
198
+ // TODO: Check if already exists
199
+ const extant = selectSetting.get(key, destination)
200
+
201
+ if (!extant) return createSetting.get(destination, key, value)
202
+ if (extant.value === value) return
203
+
204
+ updateSetting.get(value, destination, key)
205
+ const stale = staleDescendents.all(`${destination}/%`)
206
+ }
207
+
208
+
209
+ /**
210
+ * @param {string} destination
211
+ * @param {string} dependent
212
+ */
213
+ database.getSettings = (destination, dependent) => {
214
+ return Object.fromEntries(databaseSync.prepare(`SELECT * FROM settings WHERE destination = '${destination}' OR destination LIKE '${destination}/%';`)
215
+ .all()
216
+ .map(({ label, value }) => [label, value]))
217
+ }
218
+
219
+ database.getEverything = () => {
220
+ return Object.fromEntries(
221
+ databaseSync.prepare(`SELECT name FROM sqlite_master WHERE type = 'table'`)
222
+ .all()
223
+ .map(table => [
224
+ table.name,
225
+ databaseSync.prepare(`SELECT * FROM ${table.name}`).all()
226
+ ])
227
+ )
228
+ }
229
+
230
+ return database
231
+ }
232
+
233
+ /**
234
+ * @param {object} data
235
+ * @param {string} table
236
+ * @param {DatabaseSync} databaseSync
237
+ */
238
+ function updateColumns(data, table, databaseSync) {
239
+ const keys = new Set(Object.keys(data))
240
+ const columns = listColumns(table, databaseSync)
241
+ const newColumns = keys.difference(columns)
242
+ for (const column of newColumns) addColumn(column, table, databaseSync)
243
+ }
244
+
245
+ /**
246
+ * @param {string} table
247
+ * @param {DatabaseSync} databaseSync
248
+ * @description list the columns on a table.
249
+ */
250
+ function listColumns(table, databaseSync) {
251
+ const columns = new Set()
252
+ const tableInfo = databaseSync.prepare(`PRAGMA table_info(${table})`).all()
253
+ for (const column of tableInfo) columns.add(column.name)
254
+ return columns
255
+ }
256
+
257
+ /**
258
+ * @param {string} column
259
+ * @param {string} table
260
+ * @param {DatabaseSync} databaseSync
261
+ * @description Add a column to a table and update binary.
262
+ */
263
+ function addColumn(column, table, databaseSync) {
264
+ databaseSync.exec(`ALTER TABLE ${table} ADD COLUMN ${column} STRING;`)
265
+ }
266
+
267
+ const sqlCreateTables = `
268
+ CREATE TABLE IF NOT EXISTS jobs (
269
+ id INTEGER PRIMARY KEY,
270
+ pluginName TEXT NOT NULL,
271
+ data STRING NOT NULL,
272
+ done INTEGER NOT NULL DEFAULT 0,
273
+ runner STRING NOT NULL
274
+ );
275
+
276
+ -- Source/destination relationships
277
+ CREATE TABLE IF NOT EXISTS sources (
278
+ id INTEGER PRIMARY KEY,
279
+ destination STRING,
280
+ path STRING,
281
+ lastModified INTEGER
282
+ );
283
+
284
+ CREATE TABLE IF NOT EXISTS dependencies (
285
+ key INTEGER PRIMARY KEY,
286
+ destination STRING NOT NULL,
287
+ property STRING NOT NULL,
288
+ dependent STRING NOT NULL
289
+ );
290
+
291
+ CREATE TABLE IF NOT EXISTS destinations (
292
+ key INTEGER PRIMARY KEY,
293
+ path STRING UNIQUE,
294
+ dir TEXT,
295
+ syntax TEXT,
296
+ stale INTEGER,
297
+ abstract
298
+ );
299
+
300
+ CREATE TABLE IF NOT EXISTS folders (
301
+ path STRING PRIMARY KEY,
302
+ urlPath STRING
303
+ );
304
+
305
+ CREATE TABLE IF NOT EXISTS metadata (
306
+ id INTEGER PRIMARY KEY,
307
+ destination STRING,
308
+ label STRING,
309
+ value STRING,
310
+ type STRING,
311
+ UNIQUE(destination, label)
312
+ );
313
+
314
+ CREATE TABLE IF NOT EXISTS settings (
315
+ id INTEGER PRIMARY KEY,
316
+ destination STRING,
317
+ label STRING,
318
+ value STRING
319
+ );
320
+
321
+ `
322
+
323
+ export default createDatabase
package/lib/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { default as bundle } from "./bundle.js"
2
+ export { default as createDatabase } from "./createDatabase.js"
3
+ export { default as readAbstracts } from "./readAbstracts.js"
4
+ export { default as readFolders } from "./readFolders.js"
5
+ export { default as readSources } from "./readSources.js"
6
+ export { default as runJobs } from "./runJobs.js"
7
+ export { default as writeDestinations } from "./writeDestinations.js"
@@ -0,0 +1,59 @@
1
+ /** @import {VotiveConfig, FlatProcessors, Abstract, AbstractsWithSyntax, Jobs, FlatProcessor, Abstracts} from "./bundle.js" */
2
+ /** @import {Database} from "./createDatabase.js" */
3
+ /** @import {ReadSourceFileResult} from "./readSources.js"
4
+
5
+ /**
6
+ * @typedef {object} ReadAbstractsResult
7
+ * @property {Jobs} jobs
8
+ * @property {ReadAbstractResult[]} abstracts
9
+ */
10
+
11
+ /**
12
+ * @typedef {object} ReadAbstractResult
13
+ */
14
+
15
+ /**
16
+ * @param {ReadSourceFileResult} abstracts
17
+ * @param {VotiveConfig} config
18
+ * @param {Database} database
19
+ * @param {FlatProcessors} processors
20
+ * @returns {{processedAbstracts: Abstracts, abstractsJobs: Jobs}}
21
+ */
22
+ function readAbstracts(abstracts, config, database, processors) {
23
+
24
+ const processed = abstracts.flatMap(({ abstract: unprocessedAbstract, syntax }) => {
25
+
26
+ /**
27
+ * @param {FlatProcessors} processors
28
+ * @param {Abstract} abstract
29
+ * @param {Jobs} jobs
30
+ */
31
+ function recursiveProcess(processors, abstract, jobs = []) {
32
+ const [flatProcessor, ...rest] = processors
33
+ if (!flatProcessor) return { abstract, jobs }
34
+ const { processor, plugin } = flatProcessor
35
+ if (processor.syntax !== syntax
36
+ || !processor.read
37
+ || !processor.read.abstract
38
+ ) return recursiveProcess(rest, abstract, jobs)
39
+
40
+ const processed = processor.read.abstract(abstract, database, config)
41
+ processed.jobs && processed.jobs.forEach(job => job.plugin = plugin.name)
42
+ return recursiveProcess(rest, processed.abstract, [...jobs, ...(processed.jobs || [])])
43
+ }
44
+
45
+ return recursiveProcess(processors, unprocessedAbstract)
46
+ })
47
+
48
+ const processedAbstracts = []
49
+ const abstractsJobs = []
50
+
51
+ processed.forEach(({ abstract, job}) => {
52
+ processedAbstracts.push(abstract)
53
+ abstractsJobs.push(job)
54
+ })
55
+
56
+ return { processedAbstracts, abstractsJobs }
57
+ }
58
+
59
+ export default readAbstracts
@@ -0,0 +1,28 @@
1
+ /** @import {VotiveConfig, FlatProcessors, Jobs} from "./bundle.js" */
2
+ /** @import {Database} from "./createDatabase.js" */
3
+ /** @import {Dirent} from "node:fs" */
4
+
5
+ import path from "node:path"
6
+
7
+ /**
8
+ * @param {Dirent[]} folders
9
+ * @param {VotiveConfig} config
10
+ * @param {Database} database
11
+ * @param {FlatProcessors} processors
12
+ * @returns {Jobs}
13
+ */
14
+ function readFolders(folders, config, database, processors) {
15
+ if(!folders) return
16
+ return folders.flatMap(folder => {
17
+ return config.plugins.flatMap(plugin => {
18
+ return plugin.processors.flatMap(processor => {
19
+ const folderPath = path.relative(config.sourceFolder, path.join(folder.parentPath, folder.name))
20
+ return processor.read
21
+ && processor.read.folder
22
+ && processor.read.folder(folderPath, database, config)
23
+ })
24
+ })
25
+ })
26
+ }
27
+
28
+ export default readFolders
@@ -0,0 +1,142 @@
1
+ import { decodeBuffer } from "encoding-sniffer"
2
+ import fs from "node:fs/promises"
3
+ import path from "node:path"
4
+ import { visit } from "unist-util-visit"
5
+
6
+ /** @import {VotiveConfig, VotivePlugin, VotiveProcessor, FlatProcessors, Abstracts, Abstract, Jobs, ProcessorSyntax} from "./bundle.js" */
7
+ /** @import {Dirent} from "node:fs" */
8
+ /** @import {Database} from "./createDatabase.js" */
9
+
10
+ /**
11
+ * @typedef {object} ReadSourcesResult
12
+ * @property {Dirent[]} folders
13
+ * @property {ReadSourceFilePlugin[]} sources
14
+ */
15
+
16
+ /**
17
+ * @param {VotiveConfig} config
18
+ * @param {Database} database
19
+ * @param {FlatProcessors} processors
20
+ * @returns {Promise<ReadSourcesResult>}
21
+ */
22
+ async function readSources(config, database, processors) {
23
+ const dirents = await fs.readdir(config.sourceFolder, {
24
+ withFileTypes: true,
25
+ recursive: true
26
+ })
27
+
28
+ // TODO: Check file modified time
29
+
30
+ const filteredDirents = (dirents || []).filter(fileFilter(config, database))
31
+ const { files, folders } = Object.groupBy(filteredDirents, (dirent) => dirent.isFile() ? "files" : "folders")
32
+ if (!files) return { folders, sources: [] }
33
+ const readingSourceFiles = files.flatMap(readSourceFile(processors, database, config))
34
+ const sources = readingSourceFiles && await Promise.all(readingSourceFiles)
35
+ return {
36
+ folders,
37
+ sources
38
+ }
39
+ }
40
+
41
+ /**
42
+ * @param {VotiveConfig} config
43
+ * @param {Database} database
44
+ */
45
+ function fileFilter(config, database) {
46
+ /** @param {Dirent} dirent */
47
+ return (dirent) => {
48
+ if (dirent.parentPath === config.destinationFolder) {
49
+ return false
50
+ } else if (dirent.parentPath.startsWith(config.destinationFolder + path.sep)) {
51
+ return false // Ignore destination folder
52
+ } else if (dirent.name.startsWith(".")) {
53
+ return false // Ignore hidden files
54
+ } else if (dirent.parentPath.includes(path.sep + ".")) {
55
+ return false // Ignore hidden folders
56
+ }
57
+ return true
58
+ }
59
+ }
60
+
61
+ /**
62
+ * @typedef {ReadSourceFilePlugin[]} ReadSourceFileResult
63
+ */
64
+
65
+ /**
66
+ * @typedef {object} ReadSourceFilePlugin
67
+ * @property {Abstract} abstract
68
+ * @property {ProcessorSyntax} syntax
69
+ * @property {Jobs} jobs
70
+ * @property {string} destinationPath
71
+ */
72
+
73
+ /**
74
+ * @param {{ plugin: VotivePlugin, processor: VotiveProcessor }[]} processors
75
+ * @param {Database} database
76
+ * @param {VotiveConfig} config
77
+ */
78
+ function readSourceFile(processors, database, config) {
79
+ /**
80
+ * @param {import("node:fs").Dirent} dirent
81
+ * @returns {Promise<ReadSourceFilePlugin>[]}
82
+ */
83
+ return (dirent) => {
84
+ const { name, parentPath } = dirent
85
+ const filePath = path.join(parentPath, name)
86
+ const fileInfo = path.parse(filePath)
87
+
88
+ const processing = processors.flatMap(({ plugin, processor }) => process(plugin, processor, config))
89
+
90
+ /**
91
+ * @param {VotivePlugin} plugin
92
+ * @param {VotiveProcessor} processor
93
+ * @param {VotiveConfig} config
94
+ */
95
+ async function process(plugin, processor, config) {
96
+ const { read, filter, syntax } = processor
97
+ if (filter.extensions.includes(fileInfo.ext) && read && read.text) {
98
+
99
+ // Check modified time
100
+ const stat = await fs.stat(filePath)
101
+ const source = database.getSource(filePath)
102
+
103
+
104
+ if (source.lastModified === Number(stat.mtimeMs.toFixed())) return { jobs: [] }
105
+
106
+ // Get destination route
107
+ const destinationPath = route(filePath, plugin)
108
+
109
+ // Set URL if exists
110
+ const buffer = await fs.readFile(path.format(fileInfo))
111
+ const data = decodeBuffer(buffer)
112
+ const { jobs, abstract, metadata } = read.text(data, database, config)
113
+ jobs && jobs.forEach(job => job.plugin = plugin.name)
114
+ // TODO: Cache: Check if file already exists
115
+
116
+ const timeStamp = stat.mtimeMs.toFixed()
117
+ database.createSource(filePath, destinationPath, Number(timeStamp))
118
+ database.createOrUpdateDestination({
119
+ metadata,
120
+ path: destinationPath,
121
+ abstract: abstract,
122
+ syntax: syntax
123
+ })
124
+
125
+ return { abstract, destinationPath, syntax, jobs }
126
+ }
127
+ }
128
+
129
+ return processing
130
+ }
131
+ }
132
+
133
+ /**
134
+ * @param {string} filePath
135
+ * @param {VotivePlugin} plugin
136
+ */
137
+ function route(filePath, plugin) {
138
+ if (!plugin.router) return ""
139
+ return plugin.router(filePath) || ""
140
+ }
141
+
142
+ export default readSources
package/lib/runJobs.js ADDED
@@ -0,0 +1,23 @@
1
+ import workerpool from "workerpool"
2
+
3
+ /** @import {Job, Jobs, Database, VotiveConfig} from "./bundle.js" */
4
+
5
+ const pool = workerpool.pool()
6
+
7
+ /**
8
+ * @param {Jobs} jobs
9
+ * @param {VotiveConfig} config
10
+ * @param {Database} database
11
+ */
12
+ async function runJobs(jobs, config, database) {
13
+ const running = jobs.map(async job => {
14
+ if (!job) return
15
+ const plugin = config.plugins.find(plugin => plugin.name === job.plugin)
16
+ return pool.exec(plugin.runners[job.runner], [job.data])
17
+ })
18
+
19
+ const ran = await Promise.allSettled(running)
20
+ pool.terminate()
21
+ }
22
+
23
+ export default runJobs
@@ -0,0 +1,22 @@
1
+ import { styleText } from "node:util"
2
+
3
+ /** @param {string} label */
4
+ export function stopwatch(label) {
5
+ const start = performance.now()
6
+
7
+ function stop() {
8
+ const end = performance.now()
9
+ const duration = end - start
10
+ console.info(`${label}: ${styleText("red", duration.toFixed(2) + "ms")}`)
11
+ }
12
+
13
+ return stop
14
+ }
15
+
16
+ /**
17
+ * @param {string} urlPath
18
+ */
19
+ export function splitURL(urlPath) {
20
+ const [urlFileName, ...urlDirSegmentsReversed] = urlPath.split("/").reverse()
21
+ return [urlDirSegmentsReversed.reverse().join("/") || "/", urlFileName]
22
+ }
@@ -0,0 +1,42 @@
1
+ import { mkdir, writeFile } from "node:fs/promises"
2
+ import path from "node:path"
3
+
4
+ /** @import {Abstract, Abstracts, VotiveConfig} from "./bundle.js" */
5
+ /** @import {Database} from "./createDatabase.js" */
6
+
7
+
8
+ /**
9
+ * @param {VotiveConfig} config
10
+ * @param {Database} database
11
+ */
12
+ async function writeDestinations(config, database) {
13
+ const writeProcessors = config && config.plugins && config.plugins.flatMap(plugin => (
14
+ plugin.processors && plugin.processors.map(({ write, syntax }) => write && { write, syntax }).filter(a => a)
15
+ )).filter(a => a)
16
+
17
+ if (!writeProcessors || !writeProcessors.length) throw "No write processor provided"
18
+
19
+ const destinations = database.getStaleDestinations()
20
+ if (!destinations) return
21
+
22
+ const writing = destinations.flatMap(destination => {
23
+ const destinationPath = path.join(config.destinationFolder, String(destination.path))
24
+ const { dir } = path.parse(destinationPath)
25
+ return writeProcessors.map(processor => {
26
+ if (processor.syntax === destination.syntax) {
27
+ const { data, encoding = 'utf-8' } = processor.write(destination, database, config)
28
+
29
+ async function write() {
30
+ await mkdir(dir, { recursive: true })
31
+ await writeFile(destinationPath, data, encoding)
32
+ }
33
+
34
+ return write()
35
+ }
36
+ })
37
+ })
38
+
39
+ await Promise.all(writing)
40
+ }
41
+
42
+ export default writeDestinations
package/package.json CHANGED
@@ -1,12 +1,30 @@
1
1
  {
2
2
  "name": "votive",
3
- "version": "0.0.1",
4
- "description": "tk",
3
+ "version": "0.0.3",
4
+ "description": "A file processor.",
5
+ "homepage": "https://github.com/samlfair/votive#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/samlfair/votive/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/samlfair/votive.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Sam Littlefair",
15
+ "type": "module",
5
16
  "main": "index.js",
6
- "keywords": [],
7
- "author": "",
8
- "license": "ISC",
17
+ "exports": {
18
+ ".": "./lib/bundle.js",
19
+ "./internals": "./lib/index.js"
20
+ },
9
21
  "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1"
22
+ "test": "node --test --watch tests/*.js"
23
+ },
24
+ "dependencies": {
25
+ "encoding-sniffer": "^0.2.1",
26
+ "unified": "^11.0.5",
27
+ "unist-util-visit": "^5.0.0",
28
+ "workerpool": "^10.0.1"
11
29
  }
12
30
  }
Binary file
package/tests/index.js ADDED
@@ -0,0 +1,155 @@
1
+ import test from "node:test"
2
+ import assert from "node:assert/strict"
3
+ import fs from "node:fs"
4
+ import path from "node:path"
5
+ import * as votive from "votive/internals"
6
+ import { stopwatch } from "./../lib/utils/index.js"
7
+ import { readFolders, runJobs } from "../lib/index.js"
8
+
9
+ /** @import {VotiveConfig, VotivePlugin, FlatProcessors, VotiveProcessor, Runner, Job} from "./../lib/bundle.js" */
10
+
11
+ process.chdir("./tests")
12
+
13
+ test("empty directory", async () => {
14
+ const temp = fs.mkdtempDisposableSync("destination-")
15
+
16
+ try {
17
+ const stop = stopwatch("Bundle empty folder")
18
+ await votive.bundle({
19
+ sourceFolder: "./empty",
20
+ destinationFolder: temp.path
21
+ })
22
+
23
+ stop()
24
+
25
+ const dir = fs.readdirSync(temp.path)
26
+ assert(dir.length === 0, "Destination directory is not empty.")
27
+ } catch (error) {
28
+ temp.remove()
29
+ console.error(error)
30
+
31
+ }
32
+
33
+ temp.remove()
34
+ })
35
+
36
+ test("create empty database", () => {
37
+ const stop = stopwatch("Create empty database")
38
+ const database = votive.createDatabase()
39
+ stop()
40
+
41
+ database.createSource('exampleSource', 'exampleDestination', 123)
42
+ const [source] = database.getAllSources()
43
+
44
+ assert(source.destination === 'exampleDestination')
45
+ })
46
+
47
+ test("read sources", async () => {
48
+ const temp = fs.mkdtempDisposableSync("destination-")
49
+
50
+ /** @type {Job} */
51
+ const testJob = {
52
+ data: "123",
53
+ runner: "testRunner"
54
+ }
55
+
56
+ /** @type {VotiveProcessor} */
57
+ const testProcessor = {
58
+ syntax: "txt",
59
+ filter: {
60
+ extensions: [".md"]
61
+ },
62
+ read: {
63
+ path: (filePath, database, config) => undefined,
64
+ text: (text, database, config) => ({ metadata: { foo: "bar", bang: "baz" }, abstract: { baz: "bang" } }),
65
+ abstract: (abstract, database, config) => ({ abstract: { ...abstract, newAddition: true }, jobs: [testJob] }),
66
+ folder: (folder, database, config) => [testJob]
67
+ },
68
+ write: (destination, database, config) => {
69
+ return {
70
+ data: "yo there",
71
+ }
72
+ }
73
+ }
74
+
75
+ /** @type {VotivePlugin} */
76
+ const testPlugin = {
77
+ name: "test plugin",
78
+ runners: {
79
+ testRunner
80
+ },
81
+ router: (path) => path,
82
+ processors: [testProcessor]
83
+ }
84
+
85
+ /** @type {Runner} */
86
+ async function testRunner(data, database) {
87
+ const waiting = await new Promise((resolve) => setTimeout(() => resolve(data), 1000))
88
+
89
+ return waiting
90
+ }
91
+
92
+ /** @type {VotiveConfig} */
93
+ const config = {
94
+ sourceFolder: "./markdown",
95
+ destinationFolder: temp.path,
96
+ plugins: [testPlugin]
97
+ }
98
+
99
+ /** @type {FlatProcessors} */
100
+ const processors = [
101
+ {
102
+ plugin: testPlugin,
103
+ processor: testProcessor
104
+ }
105
+ ]
106
+
107
+ try {
108
+ const database = votive.createDatabase()
109
+
110
+ const moresources = database.getAllSources()
111
+ console.log({ moresources })
112
+
113
+ const stop = stopwatch("Read sources")
114
+ const { folders, sources } = await votive.readSources(config, database, processors)
115
+ stop()
116
+
117
+ const sourcesJobs = sources.flatMap(source => source.jobs)
118
+ const { processedAbstracts, abstractsJobs } = votive.readAbstracts(sources, config, database, processors)
119
+
120
+ const foldersJobs = readFolders(folders, config, database, processors)
121
+
122
+ database.createOrUpdateDestination({ metadata: { a: 1, b: 2 }, path: "abc.txt", abstract: { c: 3 }, syntax: "txt" })
123
+ const destination = database.getDestinationDependently("abc.txt", ["a"], "def")
124
+ database.createOrUpdateDestination({ metadata: { a: 3, b: 4 }, path: "def.txt", abstract: { c: 5 }, syntax: "txt" })
125
+ database.createOrUpdateDestination({ metadata: { a: 3, b: 9 }, path: "abc.txt", abstract: { c: 4 }, syntax: "txt" })
126
+ database.createOrUpdateDestination({ metadata: { a: 3, b: 9 }, path: "abc/def.txt", abstract: { c: 4 }, syntax: "txt" })
127
+
128
+
129
+ const setting = database.setSetting("abc", "theme", "blue")
130
+ const newSetting = database.setSetting("def", "category", "Dog")
131
+ const sameSetting = database.setSetting("def", "category", "Dog")
132
+ const oldSetting = database.setSetting("abc", "theme", "green")
133
+
134
+ const settings = database.getSettings("abc", "def")
135
+
136
+ const jobs = [...sourcesJobs, ...abstractsJobs, ...foldersJobs]
137
+ runJobs(jobs, config, database)
138
+
139
+ const dbResults = database.getAllSources()
140
+
141
+ const written = await votive.writeDestinations(config, database)
142
+
143
+ // const everything = database.getEverything()
144
+ // console.log(everything)
145
+
146
+ const newsources = database.getAllSources()
147
+ console.log({ newsources })
148
+
149
+ assert(true, "Read test tk")
150
+ temp.remove()
151
+ } catch (error) {
152
+ temp.remove()
153
+ throw error
154
+ }
155
+ })
File without changes
File without changes
File without changes
package/index.js DELETED
@@ -1 +0,0 @@
1
- console.log("tk")