imdone-core 2.0.0 → 2.0.1

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.
Files changed (160) hide show
  1. package/.dependency-cruiser.cjs +412 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  3. package/.github/workflows/ci.yml +24 -0
  4. package/.imdoneignore +13 -0
  5. package/.mocharc.json +10 -0
  6. package/.prettierrc +6 -0
  7. package/CHANGELOG.md +160 -0
  8. package/_index.md +7 -0
  9. package/_navbar.md +1 -0
  10. package/build.js +57 -0
  11. package/esm-migration.md +446 -0
  12. package/index.html +184 -0
  13. package/lib/FileProjectContext.js +13 -0
  14. package/lib/ProjectContext.js +43 -0
  15. package/lib/adapters/date-time.js +15 -0
  16. package/lib/adapters/environment.js +3 -0
  17. package/lib/adapters/file-gateway.js +154 -0
  18. package/lib/adapters/git-download.js +55 -0
  19. package/lib/adapters/index.js +9 -0
  20. package/lib/adapters/logger.js +136 -0
  21. package/lib/adapters/markdown.js +180 -0
  22. package/lib/adapters/parsers/DateLanguageParser.js +12 -0
  23. package/lib/adapters/parsers/content-transformer.js +116 -0
  24. package/lib/adapters/parsers/file/LocalFileParserFactory.js +36 -0
  25. package/lib/adapters/parsers/file/ReadStreamCodeParser.js +126 -0
  26. package/lib/adapters/parsers/file/ReadStreamMarkdownParser.js +17 -0
  27. package/lib/adapters/parsers/file/ReadStreamTaskParser.js +106 -0
  28. package/lib/adapters/parsers/file/index.js +4 -0
  29. package/lib/adapters/parsers/function-parser.js +25 -0
  30. package/lib/adapters/parsers/index.js +5 -0
  31. package/lib/adapters/parsers/task/CardContentParser.js +367 -0
  32. package/lib/adapters/parsers/task/CheckStyleTaskParser.js +28 -0
  33. package/lib/adapters/parsers/task/CodeStyleTaskParser.js +24 -0
  34. package/lib/adapters/parsers/task/HashStyleTaskParser.js +27 -0
  35. package/lib/adapters/parsers/task/MarkdownStyleTaskParser.js +24 -0
  36. package/lib/adapters/parsers/task/TaskParser.js +38 -0
  37. package/lib/adapters/parsers/task/index.js +6 -0
  38. package/lib/adapters/storage/config.js +44 -0
  39. package/lib/adapters/storage/index.js +2 -0
  40. package/lib/adapters/storage/tags.js +29 -0
  41. package/lib/adapters/yaml.js +9 -0
  42. package/lib/card.js +299 -0
  43. package/lib/checksum.js +22 -0
  44. package/lib/config.js +249 -0
  45. package/lib/constants.js +68 -0
  46. package/lib/context/ApplicationContext.js +35 -0
  47. package/lib/context/index.js +1 -0
  48. package/lib/default-settings.js +37 -0
  49. package/lib/domain/entities/View.js +23 -0
  50. package/lib/domain/entities/index.js +1 -0
  51. package/lib/domain/index.js +2 -0
  52. package/lib/domain/usecases/GetViewsUsecase.js +44 -0
  53. package/lib/domain/usecases/SaveViewUsecase.js +51 -0
  54. package/lib/domain/usecases/index.js +2 -0
  55. package/lib/file.js +1391 -0
  56. package/lib/get-tasks-to-modify.js +117 -0
  57. package/lib/languages.js +113 -0
  58. package/lib/list.js +56 -0
  59. package/lib/migrate-config.js +165 -0
  60. package/lib/mixins/index.js +2 -0
  61. package/lib/mixins/repo-fs-store.js +359 -0
  62. package/lib/mixins/repo-watched-fs-store.js +206 -0
  63. package/lib/plugins/archive-plugin.js +72 -0
  64. package/lib/plugins/default-board-actions-plugin.js +56 -0
  65. package/lib/plugins/default-board-properties-plugin.js +101 -0
  66. package/lib/plugins/default-plugins.js +7 -0
  67. package/lib/plugins/epic-plugin.js +177 -0
  68. package/lib/plugins/extension-plugin.js +94 -0
  69. package/lib/plugins/index.js +9 -0
  70. package/lib/plugins/persist-tags-plugin.js +14 -0
  71. package/lib/plugins/plugin-manager.js +458 -0
  72. package/lib/plugins/plugin-registry.js +57 -0
  73. package/lib/project-factory.js +42 -0
  74. package/lib/project.js +864 -0
  75. package/lib/repository.js +1514 -0
  76. package/lib/task.js +780 -0
  77. package/lib/tools.js +176 -0
  78. package/lib/usecases/actions.js +32 -0
  79. package/lib/usecases/get-imdone-url.js +6 -0
  80. package/lib/usecases/get-project-tags.js +7 -0
  81. package/lib/usecases/get-tasks-in-file.js +26 -0
  82. package/lib/usecases/index.js +4 -0
  83. package/package.json +2 -6
  84. package/scripts/delete-markdown-indexes.js +87 -0
  85. package/scripts/generate-index.js +35 -0
  86. package/scripts/index-markdown.js +55 -0
  87. package/tsconfig.json +15 -0
  88. package/types/FileProjectContext.d.ts +4 -0
  89. package/types/ProjectContext.d.ts +9 -0
  90. package/types/adapters/date-time.d.ts +2 -0
  91. package/types/adapters/environment.d.ts +1 -0
  92. package/types/adapters/file-gateway.d.ts +51 -0
  93. package/types/adapters/git-download.d.ts +1 -0
  94. package/types/adapters/index.d.ts +9 -0
  95. package/types/adapters/logger.d.ts +24 -0
  96. package/types/adapters/markdown.d.ts +13 -0
  97. package/types/adapters/parsers/DateLanguageParser.d.ts +1 -0
  98. package/types/adapters/parsers/content-transformer.d.ts +10 -0
  99. package/types/adapters/parsers/file/LocalFileParserFactory.d.ts +5 -0
  100. package/types/adapters/parsers/file/ReadStreamCodeParser.d.ts +13 -0
  101. package/types/adapters/parsers/file/ReadStreamMarkdownParser.d.ts +7 -0
  102. package/types/adapters/parsers/file/ReadStreamTaskParser.d.ts +19 -0
  103. package/types/adapters/parsers/file/index.d.ts +4 -0
  104. package/types/adapters/parsers/function-parser.d.ts +1 -0
  105. package/types/adapters/parsers/index.d.ts +5 -0
  106. package/types/adapters/parsers/task/CardContentParser.d.ts +45 -0
  107. package/types/adapters/parsers/task/CheckStyleTaskParser.d.ts +12 -0
  108. package/types/adapters/parsers/task/CodeStyleTaskParser.d.ts +14 -0
  109. package/types/adapters/parsers/task/HashStyleTaskParser.d.ts +13 -0
  110. package/types/adapters/parsers/task/MarkdownStyleTaskParser.d.ts +14 -0
  111. package/types/adapters/parsers/task/TaskParser.d.ts +7 -0
  112. package/types/adapters/parsers/task/index.d.ts +6 -0
  113. package/types/adapters/storage/config.d.ts +6 -0
  114. package/types/adapters/storage/index.d.ts +2 -0
  115. package/types/adapters/storage/tags.d.ts +3 -0
  116. package/types/adapters/yaml.d.ts +2 -0
  117. package/types/card.d.ts +166 -0
  118. package/types/checksum.d.ts +8 -0
  119. package/types/config.d.ts +61 -0
  120. package/types/constants.d.ts +46 -0
  121. package/types/context/ApplicationContext.d.ts +24 -0
  122. package/types/context/index.d.ts +1 -0
  123. package/types/default-settings.d.ts +2 -0
  124. package/types/domain/entities/FileProjectContext.d.ts +5 -0
  125. package/types/domain/entities/View.d.ts +17 -0
  126. package/types/domain/entities/index.d.ts +1 -0
  127. package/types/domain/index.d.ts +2 -0
  128. package/types/domain/usecases/GetViewsUsecase.d.ts +4 -0
  129. package/types/domain/usecases/SaveViewUsecase.d.ts +8 -0
  130. package/types/domain/usecases/index.d.ts +2 -0
  131. package/types/file.d.ts +344 -0
  132. package/types/get-tasks-to-modify.d.ts +3 -0
  133. package/types/index.d.ts +23 -0
  134. package/types/languages.d.ts +649 -0
  135. package/types/list.d.ts +30 -0
  136. package/types/migrate-config.d.ts +1 -0
  137. package/types/mixins/index.d.ts +2 -0
  138. package/types/mixins/repo-fs-store.d.ts +2 -0
  139. package/types/mixins/repo-watched-fs-store.d.ts +2 -0
  140. package/types/plugins/archive-plugin.d.ts +8 -0
  141. package/types/plugins/default-board-actions-plugin.d.ts +9 -0
  142. package/types/plugins/default-board-properties-plugin.d.ts +16 -0
  143. package/types/plugins/default-plugins.d.ts +6 -0
  144. package/types/plugins/epic-plugin.d.ts +19 -0
  145. package/types/plugins/extension-plugin.d.ts +15 -0
  146. package/types/plugins/index.d.ts +9 -0
  147. package/types/plugins/persist-tags-plugin.d.ts +4 -0
  148. package/types/plugins/plugin-manager.d.ts +68 -0
  149. package/types/plugins/plugin-registry.d.ts +1 -0
  150. package/types/project-factory.d.ts +14 -0
  151. package/types/project.d.ts +158 -0
  152. package/types/repository.d.ts +996 -0
  153. package/types/task.d.ts +202 -0
  154. package/types/tools.d.ts +29 -0
  155. package/types/usecases/actions.d.ts +7 -0
  156. package/types/usecases/get-imdone-url.d.ts +1 -0
  157. package/types/usecases/get-project-tags.d.ts +1 -0
  158. package/types/usecases/get-tasks-in-file.d.ts +4 -0
  159. package/types/usecases/index.d.ts +4 -0
  160. package/vitest.config.js +13 -0
package/lib/project.js ADDED
@@ -0,0 +1,864 @@
1
+ import { Project } from 'imdone-api/lib/project.js';
2
+ import { PluginManager } from './plugins/plugin-manager.js';
3
+ import { Repository } from './repository.js';
4
+ import _cloneDeep from 'lodash.clonedeep';
5
+ import { newCard } from './card.js';
6
+ import { constants } from './constants.js';
7
+ const { JOURNAL_TYPE, CONFIG_DIR } = constants;
8
+ import _path from 'node:path';
9
+ import moment from 'moment';
10
+ import { getIsoDateWithOffset } from './adapters/date-time.js';
11
+ import * as fileGateway from './adapters/file-gateway.js';
12
+ import matter from 'gray-matter';
13
+ import _isObject from 'lodash.isobject';
14
+ import _isString from 'lodash.isstring';
15
+ import _isFunction from 'lodash.isfunction';
16
+ import { exec } from 'node:child_process';
17
+ import { sort } from 'fast-sort';
18
+ import { Task } from './task.js';
19
+ import eol from 'eol';
20
+ import { renderMarkdown, extractWikilinkTopics } from './adapters/markdown.js';
21
+ import { getFunctionSignature } from './adapters/parsers/function-parser.js';
22
+ import { format } from './adapters/parsers/content-transformer.js';
23
+ import { logger } from './adapters/logger.js';
24
+
25
+ function calculateTotals(lists = []) {
26
+ const totals = {}
27
+ lists.forEach((list) => {
28
+ try {
29
+ totals[list.name] = list.tasks.length
30
+ } catch (e) {
31
+ logger.error('Error calculating list totals:', e)
32
+ }
33
+ })
34
+ return totals
35
+ }
36
+
37
+ function onChange(project, event, data) {
38
+ project.emit(event, data)
39
+ }
40
+
41
+ const EVENTS = [
42
+ 'file.processed',
43
+ 'files.found',
44
+ 'file.update',
45
+ 'file.saved',
46
+ 'list.modified',
47
+ 'config.update',
48
+ ]
49
+
50
+ export class WorkerProject extends Project {
51
+ constructor(repo) {
52
+ super()
53
+ this.repo = repo
54
+ this.innerFilter = ''
55
+ this.fileGateway = fileGateway
56
+ this._updatedAt = undefined
57
+ this.data = {}
58
+ this.dataKeys = []
59
+ this.repo.project = this
60
+ this.pluginManager = new PluginManager(this)
61
+ }
62
+
63
+ get configDir() {
64
+ return _path.join(this.path, CONFIG_DIR)
65
+ }
66
+
67
+ get allTopics() {
68
+ return [...this.repo.allTopics]
69
+ }
70
+
71
+ get allTags() {
72
+ return [...this.repo.allTags]
73
+ }
74
+
75
+ get allContexts() {
76
+ return [...this.repo.allContexts]
77
+ }
78
+
79
+ get allMeta() {
80
+ return this.repo.allMeta
81
+ }
82
+
83
+ get lists() {
84
+ return this.getLists({
85
+ tasks: this.filteredCards,
86
+ })
87
+ }
88
+
89
+ get allLists() {
90
+ return this.getLists()
91
+ }
92
+
93
+ get filteredCards() {
94
+ return this.getCards(this.filter)
95
+ }
96
+
97
+ get isoDateWithOffset() {
98
+ return getIsoDateWithOffset()
99
+ }
100
+
101
+ get updatedAt() {
102
+ return this._updatedAt
103
+ }
104
+
105
+ get files() {
106
+ return this.repo.getFilePaths()
107
+ }
108
+
109
+ get config() {
110
+ return this.repo.config
111
+ }
112
+
113
+ get defaultFilter() {
114
+ return this.config && this.config.defaultFilter
115
+ }
116
+
117
+ set defaultFilter(filter) {
118
+ this.config.defaultFilter = filter
119
+ }
120
+
121
+ get totals() {
122
+ return calculateTotals(this.lists)
123
+ }
124
+
125
+ get path() {
126
+ return this.repo.path
127
+ }
128
+
129
+ get name() {
130
+ return this.repo.getDisplayName()
131
+ }
132
+
133
+ get doneList() {
134
+ return this.config.getDoneList()
135
+ }
136
+
137
+ get boardActions() {
138
+ return [
139
+ ...this.pluginManager.getBoardActions(),
140
+ ]
141
+ }
142
+
143
+ get filter() {
144
+ return this.innerFilter
145
+ }
146
+
147
+ set filter(filter) {
148
+ this.innerFilter = filter
149
+ }
150
+
151
+ async init() {
152
+ this.pluginManager.on('plugin-installed', () => this.emitUpdate())
153
+ this.pluginManager.on('plugin-uninstalled', () => this.emitUpdate())
154
+ this.pluginManager.on('plugins-reloaded', () => this.emitUpdate())
155
+ await this.pluginManager.loadPlugins()
156
+ this.data = await this.pluginManager.getBoardProperties()
157
+ this.dataKeys = this.getDataKeys(this.data)
158
+
159
+ logger.log('data', this.data)
160
+ logger.log('dataKeys', this.dataKeys)
161
+ EVENTS.forEach((event) => {
162
+ this.repo.on(event, (data) => onChange(this, event, data))
163
+ })
164
+
165
+ this.repo.on('task.found', (task) => this.pluginManager.onTaskFound(task))
166
+
167
+ const files = await this.repo.init()
168
+
169
+ try {
170
+ await this.pluginManager.startDevMode()
171
+ } catch (err) {
172
+ logger.log('Error on starting dev mode', err)
173
+ throw new Error('Error on starting dev mode', { cause: err })
174
+ }
175
+
176
+ return files
177
+ }
178
+
179
+ initIndexes(allLists) {
180
+ allLists.forEach((list) => {
181
+ list.tasks.filter(list => !list.filter).forEach((task, index) => {
182
+ task.index = index
183
+ })
184
+ })
185
+ }
186
+
187
+ // HACK To handle circular dependency issue with file
188
+ // <!--
189
+ // order:0
190
+ // -->
191
+ toJSON() {
192
+ return { path: this.path }
193
+ }
194
+
195
+ getListsForImdoneJSON() {
196
+ this.initIndexes(this.repo.getTasksByList())
197
+ let allLists = this.getLists({
198
+ populateFiltered: true,
199
+ })
200
+ const totals = calculateTotals(allLists)
201
+ allLists = allLists.map((list) => {
202
+ list = { ...list }
203
+ list.tasks = list.tasks.map((card, index) => {
204
+ card.interpretedContent = ''
205
+ card.totals = totals
206
+ return card
207
+ })
208
+ return list
209
+ })
210
+
211
+ return { allLists, totals }
212
+ }
213
+
214
+ getInitializedCards(allLists, totals) {
215
+ return allLists
216
+ .map((list) => list.tasks)
217
+ .flat()
218
+ .map(
219
+ (card) => card.init(totals)
220
+ )
221
+ }
222
+
223
+ async toImdoneJSON() {
224
+ this.pluginManager.initDevMode()
225
+ logger.time('toJSON time')
226
+ this._updatedAt = new Date()
227
+ const { allLists , totals } = this.getListsForImdoneJSON()
228
+
229
+ logger.time('plugin onBeforeBoardUpdate time')
230
+ await this.pluginManager.onBeforeBoardUpdate()
231
+ logger.timeEnd('plugin onBeforeBoardUpdate time')
232
+
233
+ logger.time('getBoardProperties time')
234
+ const data = this.data = await this.pluginManager.getBoardProperties()
235
+ logger.timeEnd('getBoardProperties time')
236
+
237
+ logger.time('getDataKeys time')
238
+ const dataKeys = this.dataKays = this.getDataKeys(data)
239
+ logger.timeEnd('getDataKeys time')
240
+
241
+ logger.time('card init time')
242
+ const cards = this.getInitializedCards(allLists, totals)
243
+ logger.timeEnd('card init time')
244
+
245
+ logger.time('plugin onBoardUpdate time')
246
+ await this.pluginManager.onBoardUpdate(allLists)
247
+ logger.timeEnd('plugin onBoardUpdate time')
248
+
249
+ logger.time('getTags time')
250
+ const tags = this.getTags(cards)
251
+ logger.timeEnd('getTags time')
252
+
253
+ logger.time('getLists time')
254
+ const lists = this.filter
255
+ ? this.getRequestedLists(cards)
256
+ : allLists
257
+ logger.timeEnd('getLists time')
258
+
259
+ logger.timeEnd('toJSON time')
260
+ return {
261
+ path: this.path,
262
+ config: this.config,
263
+ lists: lists,
264
+ files: this.files,
265
+ totals,
266
+ totalCards: this.repo.getTasks().length,
267
+ tags,
268
+ allMeta: this.allMeta,
269
+ allContexts: this.allContexts,
270
+ allTags: this.allTags,
271
+ filter: this.filter,
272
+ defaultFilter: this.defaultFilter,
273
+ actions: this.boardActions,
274
+ plugins: this.pluginManager.getPlugins(),
275
+ data,
276
+ dataKeys,
277
+ queryProps: this.getQueryProps(cards),
278
+ }
279
+ }
280
+
281
+ getRequestedLists(cards) {
282
+ return this.getLists({
283
+ tasks: this.getCards(this.filter, cards),
284
+ populateFiltered: true,
285
+ }).map(list => {
286
+ const configList = this.config.lists.find(l => l.name === list.name)
287
+ list.hidden = configList && configList.hidden
288
+ if (this.hideLists.includes(list.name)) {
289
+ list.hidden = true
290
+ }
291
+ return list
292
+ })
293
+ }
294
+
295
+ emit() {} // This is implemented in imdone UI worker
296
+
297
+ emitUpdate() {
298
+ this.emit('file.update')
299
+ }
300
+
301
+ async destroy() {
302
+ await this.repo.destroy()
303
+ if (this.pluginManager) this.pluginManager.destroyPlugins()
304
+ }
305
+
306
+ getDataKeys(data) {
307
+ const keys = Object.keys(data).map((key) => {
308
+ const value = data[key]
309
+ if (_isFunction(value)) {
310
+ key = getFunctionSignature(value)
311
+ }
312
+ return key
313
+ })
314
+ keys.forEach((key) => {
315
+ const value = data[key]
316
+ if (_isObject(value) && !Array.isArray(value)) {
317
+ keys.push(...Object.keys(value).map((k) => `${key}.${k}`))
318
+ }
319
+ })
320
+ return keys
321
+ }
322
+
323
+ removeList(list) {
324
+ this.repo.removeList(list)
325
+ }
326
+
327
+ getLists(opts) {
328
+ const {tasks = this.getDefaultFilteredCards(), populateFiltered = false} = opts || {}
329
+ return Repository.getTasksByList(this.repo, tasks, true, populateFiltered)
330
+ }
331
+
332
+ getTaskQueryProps(props) {
333
+ const queryProps = []
334
+ if (props) {
335
+ // add all properties of the task to the queryProps array
336
+ Object.keys(props).forEach((key) => {
337
+ if (key === 'data') return
338
+ if (queryProps.includes(key)) return
339
+
340
+ queryProps.push(key)
341
+
342
+ if (_isObject(props[key])) {
343
+ Object.keys(props[key]).forEach((subKey) => {
344
+ if (!queryProps.includes(`${key}.${subKey}`))
345
+ queryProps.push(`${key}.${subKey}`)
346
+ })
347
+ }
348
+ })
349
+ }
350
+ return queryProps
351
+ }
352
+
353
+ getQueryProps(cards) {
354
+ if (!cards) return []
355
+ const queryProps = this.getTaskQueryProps(cards[0])
356
+ return [
357
+ ...new Set(queryProps),
358
+ ...[...this.repo.metaKeys].map(key => `allMeta.${key}`),
359
+ ...[...this.repo.metaKeys].map(key => `meta.${key}`),
360
+ ...this.allTopics,
361
+ ...this.allTags,
362
+ ...this.allContexts,
363
+ ]
364
+ }
365
+
366
+
367
+ getTags(cards = this.getDefaultFilteredCards()) {
368
+ const tags = []
369
+ cards.forEach((card) => {
370
+ card.allTags.forEach((tag) => {
371
+ let foundTag = tags.find(({name}) => tag === name)
372
+ if (!foundTag) {
373
+ foundTag = {name: tag, count: 1}
374
+ tags.push(foundTag)
375
+ }
376
+ foundTag.count++
377
+ })
378
+ })
379
+
380
+ return sort(tags).by([
381
+ {desc: (tag) => tag.count},
382
+ {asc: (tag) => tag.name},
383
+ ])
384
+ }
385
+
386
+ getDefaultFilteredCards() {
387
+ return this.filterCards(this.repo.getTasks(), this.defaultFilter)
388
+ }
389
+
390
+ getAllCards(filter) {
391
+ const allTasks = this.repo.getTasks()
392
+ return this.getCards(filter, allTasks)
393
+ }
394
+
395
+ getCards(filter, cards = this.getDefaultFilteredCards()) {
396
+ cards = filter ? this.filterCards(cards, filter) : cards
397
+ return cards
398
+ }
399
+
400
+ filterCards(cards, filter) {
401
+ this.hideLists = []
402
+ const {
403
+ result,
404
+ hideLists
405
+ } = Repository.filterCards(cards, filter)
406
+ this.hideLists = hideLists
407
+ return result
408
+ }
409
+
410
+ async addMetadata(task, key, value) {
411
+ if (task.hasMetaData(key, value)) return
412
+ if (!task.allMeta[key]) task.allMeta[key] = []
413
+ task.allMeta[key].push(value)
414
+ if (!/^['"]/.test(value) && /\s/.test(value)) value = `"${value}"`
415
+ const metaData = `${key}${this.config.getMetaSep()}${value}`
416
+ const content = task.addToLastCommentInContent(
417
+ task.content,
418
+ metaData,
419
+ this.config.isMetaNewLine()
420
+ )
421
+ return await this.updateCardContent(task, content)
422
+ }
423
+
424
+ async removeMetadata(task, key, value) {
425
+ if (!task.meta[key]) return
426
+ const file = this.getFileForTask(task)
427
+ const content = file.removeMetaData(task.content, key, value)
428
+ return await this.updateCardContent(task,content)
429
+ }
430
+
431
+ async addTag(task, tag) {
432
+ if (task.tags.includes(tag)) return
433
+ task.allTags.push(tag)
434
+ const tagContent = `${this.config.getTagPrefix()}${tag}`
435
+ const content = task.addToLastCommentInContent(
436
+ task.content,
437
+ tagContent,
438
+ this.config.isMetaNewLine()
439
+ )
440
+ return await this.updateCardContent(task, content)
441
+ }
442
+
443
+ async removeTag(task, tag) {
444
+ if (!task.tags.includes(tag)) return
445
+ const tagContent = new RegExp(
446
+ `\\${this.config.getTagPrefix()}${tag}\\s`,
447
+ 'g'
448
+ )
449
+ logger.log('removeTag regex:', tagContent)
450
+ const content = task.content.replace(tagContent, '')
451
+ return await this.updateCardContent(task, content)
452
+ }
453
+
454
+ async moveTask(task, newList, newPos) {
455
+ return this.repo.moveTask({task, newList, newPos})
456
+ }
457
+
458
+ getFile(filePath) {
459
+ return this.repo.getFile(filePath)
460
+ }
461
+
462
+ getFileForTask(task) {
463
+ return this.repo.getFileForTask(task)
464
+ }
465
+
466
+ rollBackFileForTask(task) {
467
+ return this.getFileForTask(task).rollback().extractTasks(this.config)
468
+ }
469
+
470
+ async updateCardContent(task, content) {
471
+ const file = await this.repo.modifyTaskFromContent(task, content)
472
+ this.emitUpdate()
473
+ return file
474
+ }
475
+
476
+ async snackBar({ message, type, duration }) {
477
+ this.emit('project.snackBar', { message, type, duration })
478
+ }
479
+
480
+ async toast({ message, type, duration }) {
481
+ this.emit('project.toast', { message, type, duration })
482
+ }
483
+
484
+ filterLists(filter, lists = this.lists) {
485
+ return lists.map((list) => {
486
+ let newList = { ...list, tasks: [] }
487
+ newList = _cloneDeep(newList)
488
+ newList.tasks = Repository.query(list.tasks, filter)
489
+ return newList
490
+ })
491
+ }
492
+
493
+ async copyToClipboard(text, message) {this.files
494
+ this.emit('project.copyToClipboard', { text, message })
495
+ }
496
+
497
+ async openUrl(url) {
498
+ this.emit('project.openUrl', url)
499
+ }
500
+
501
+ // TODO For methods that emit, they should still do the thing without imdone UI
502
+ // <!--
503
+ // #urgent
504
+ // #important
505
+ // order:-195
506
+ // -->
507
+ async openPath(path) {
508
+ this.emit('project.openPath', path)
509
+ }
510
+
511
+ saveFile(content, file) {
512
+ const filePath = this.getFullPath(file)
513
+ this.emit('project.saveFile', { file: filePath, content })
514
+ }
515
+
516
+ /*
517
+ * @param {Object} opts - The options object
518
+ * @param {String} opts.list - The list name to add the card to
519
+ * @param {String} opts.path - The path to the file to add the card to
520
+ * @param {String} opts.template - The template to use for the new card
521
+ * @param {String} opts.title - The title of the new card
522
+ * @param {String} opts.comments - The comments to add to the new card
523
+ * @param {Boolean} opts.emit - Whether to emit the new card event
524
+ */
525
+ async newCard({ list, path, template, title, comments, emit = true }) {
526
+ if (!path || !_path.parse(path).ext) path = this.getNewCardsFile({title})
527
+ path = this.getFullPath(path)
528
+
529
+ const { isFile, isDirectory } = await fileGateway.preparePathForWriting(path)
530
+
531
+ if (!template) template = this.getNewCardTemplate(path, isFile)
532
+
533
+ const boardData = await this.pluginManager.getBoardProperties();
534
+ template = format(template, boardData)
535
+
536
+ if (comments) template = Task.addToLastCommentInContent(template, comments, this.config.isMetaNewLine())
537
+
538
+ let relativePath = _path.relative(this.path, path)
539
+ if (isDirectory) relativePath += _path.sep
540
+
541
+ const data = {
542
+ list,
543
+ path,
544
+ relativePath,
545
+ template,
546
+ isDirectory,
547
+ }
548
+ if (emit) this.emit('project.newCard', data)
549
+ return data
550
+ }
551
+
552
+ async addCardToFile(opts) {
553
+ return await this.addTaskToFile(opts)
554
+ }
555
+
556
+ async addTaskToFile({path, list = this.config?.getDefaultList(), content, tags = [], contexts = [], meta = [], useCardTemplate = false}) {
557
+ const pluginMods = await this.pluginManager.onBeforeAddTask({path, list, meta, tags, contexts, content, useCardTemplate})
558
+ path = pluginMods.path
559
+ content = pluginMods.content
560
+ meta = pluginMods.meta
561
+ tags = pluginMods.tags
562
+ contexts = pluginMods.contexts
563
+ const cardData = await this.newCard({list, path, title: eol.split(content)[0], emit: false})
564
+ const filePath = cardData.path
565
+ if (useCardTemplate) content += cardData.template
566
+
567
+ const boardData = await this.pluginManager.getBoardProperties();
568
+ const data = { ...boardData, ...cardData}
569
+ content = format(content, data)
570
+
571
+ content = this.addTagsToContent(tags, content)
572
+ content = this.addContextsToContent(contexts, content)
573
+ content = this.addMetaToContent(meta, content)
574
+
575
+ return await this.repo.addTaskToFile(filePath, list, content)
576
+ }
577
+
578
+ addMetaToContent(meta, content) {
579
+ if (meta && meta.length > 0) {
580
+ let metaContent = ''
581
+ meta.forEach(({ key, value }) => {
582
+ if (!/^['"]/.test(value) && /\s/.test(value)) value = `"${value}"`
583
+ const metaData = `${key}${this.repo.config.getMetaSep()}${value}`
584
+ if (content.includes(metaData)) return
585
+ const existingMetadata = Task.parseMetaData(this.config, content)[key]
586
+ if (existingMetadata) {
587
+ content = Task.removeMetaData({
588
+ config: this.config,
589
+ content,
590
+ key,
591
+ value: existingMetadata[0],
592
+ })
593
+ }
594
+ const spaceOrNewLine = this.config.isMetaNewLine() ? '\n' : ' '
595
+ metaContent = `${metaContent}${spaceOrNewLine}${metaData}`
596
+ })
597
+ content = Task.addToLastCommentInContent(content, metaContent.trim(), this.config.isMetaNewLine())
598
+ }
599
+ return content
600
+ }
601
+
602
+ addContextsToContent(contexts, content) {
603
+ if (contexts && contexts.length > 0) {
604
+ let contextContent = ''
605
+ contexts.forEach(context => {
606
+ const contextWithPrefix = `@${context}`
607
+ if (content.includes(contextWithPrefix)) return
608
+ contextContent = `${contextContent} @${context}`
609
+ })
610
+ content = Task.addToLastCommentInContent(content, contextContent.trim(), this.config.isMetaNewLine())
611
+ }
612
+ return content
613
+ }
614
+
615
+ addTagsToContent(tags, content) {
616
+ if (tags && tags.length > 0) {
617
+ let tagContent = ''
618
+ tags.forEach(tag => {
619
+ const tagWithPrefix = `${this.config.getTagPrefix()}${tag}`
620
+ if (content.includes(tagWithPrefix)) return
621
+ tagContent = `${tagContent} ${tagWithPrefix}`
622
+ })
623
+ content = Task.addToLastCommentInContent(content, tagContent.trim(), this.config.isMetaNewLine())
624
+ }
625
+ return content
626
+ }
627
+
628
+ async deleteTask(task) {
629
+ await this.repo.deleteTask(task)
630
+ await this.pluginManager.onAfterDeleteTask(task)
631
+ }
632
+
633
+ async deleteTasks(tasks) {
634
+ await this.repo.deleteTasks(tasks)
635
+ }
636
+
637
+ setFilter(filter) {
638
+ this.emit('project.filter', { filter })
639
+ }
640
+
641
+ getNewCardTemplate(file, isFile) {
642
+ const frontMatter = this.getNewCardFileFrontMatter(file, isFile)
643
+ const card = newCard(
644
+ {
645
+ frontMatter,
646
+ repoId: this.path,
647
+ text: '',
648
+ source: { path: this.getNewCardsFile() },
649
+ },
650
+ this,
651
+ true
652
+ )
653
+ card.init(this.totals)
654
+ return card.formatContent(frontMatter.template).content
655
+ }
656
+
657
+ getNewCardFileFrontMatter(file, isFile) {
658
+ let fileContent = ''
659
+ if (file && isFile) {
660
+ fileContent = fileGateway.readFileSync(file, 'utf8')
661
+ } else if (JOURNAL_TYPE.NEW_FILE === this.config.journlType) {
662
+ fileContent = this.config.journalTemplate
663
+ }
664
+ let { props, computed, template } = this.config.settings.cards
665
+ const frontMatter = matter(fileContent).data || {}
666
+ if (!_isObject(props)) props = {}
667
+ if (!_isObject(computed)) computed = {}
668
+ if (!_isString(template)) template = ''
669
+ if (!_isObject(frontMatter.props)) frontMatter.props = {}
670
+ if (!_isObject(frontMatter.computed)) frontMatter.computed = {}
671
+ if (!_isString(frontMatter.template)) frontMatter.template = template
672
+ props = {
673
+ ...props,
674
+ ...frontMatter.props,
675
+ now: new Date().toDateString(),
676
+ totals: this.totals,
677
+ }
678
+ computed = { ...computed, ...frontMatter.computed }
679
+ return {
680
+ ...frontMatter,
681
+ props,
682
+ computed,
683
+ template: frontMatter.template,
684
+ }
685
+ }
686
+
687
+ getNewCardsFile(opts = { relPath: false }) {
688
+ const { relPath, title } = opts
689
+ if (!this.config) return ''
690
+ const filePath = this.appendNewCardsTo(title)
691
+ if (!filePath) return ''
692
+ return filePath
693
+ }
694
+
695
+ appendNewCardsTo(title) {
696
+ const journalType = this.config.journalType
697
+ if (journalType === JOURNAL_TYPE.SINGLE_FILE)
698
+ return this.getFullPath(this.config.appendNewCardsTo)
699
+ if (journalType === JOURNAL_TYPE.FOLDER)
700
+ return this.getJournalFile().fullFilePath
701
+ if (journalType === JOURNAL_TYPE.NEW_FILE) {
702
+ if (!title) return this.getFullPath(this.config.journalPath)
703
+ const fileName = `${this.sanitizeFileName(title)}.md`
704
+ const fileFolder = this.getFullPath(this.config.journalPath)
705
+ const filePath = _path.join(fileFolder, fileName)
706
+
707
+ return filePath
708
+ }
709
+ }
710
+
711
+ sanitizeFileName(fileName) {
712
+ return fileGateway.sanitizeFileName(fileName, this.config.replaceSpacesWith)
713
+ }
714
+
715
+ getJournalFile() {
716
+ const month = moment().format('YYYY-MM')
717
+ const today = moment().format('YYYY-MM-DD')
718
+ const journalPath = this.config.journalPath
719
+ const folderPath = _path.join(journalPath, month)
720
+ const journalFilePrefix = this.config.journalFilePrefix
721
+ const journalFileSuffix = this.config.journalFileSuffix
722
+ const filePath = _path.join(
723
+ folderPath,
724
+ `${journalFilePrefix}${today}${journalFileSuffix}.md`
725
+ )
726
+
727
+ const fullFilePath = this.getFullPath(filePath)
728
+ return { filePath, fullFilePath }
729
+ }
730
+
731
+ getFullPath(...path) {
732
+ if (_path.join(...path).startsWith(this.path)) {
733
+ return _path.join(...path)
734
+ }
735
+ return _path.join.apply({}, [this.path, ...path])
736
+ }
737
+
738
+ performCardAction(action, task) {
739
+ task = this.repo.getTask(task.id)
740
+ try {
741
+ action = JSON.parse(action)
742
+ } catch (e) {
743
+ //
744
+ }
745
+ if (action.plugin) return this.pluginManager.performCardAction(action, task)
746
+
747
+ const actionFunction = task.getCardActions()[action.index].action
748
+ // TODO These card actions should be removed
749
+ // #imdone-1.54.0
750
+ // <!--
751
+ // order:-175
752
+ // -->
753
+ const actions = {
754
+ filter: (filter) => {
755
+ this.setFilter(filter)
756
+ },
757
+ newCard: async (list, path) => {
758
+ if (task.source.lang !== 'text')
759
+ return this.alert('Unable to append cards in code files.')
760
+ if (path) path = this.getFullPath(path)
761
+ await this.newCard({ list, path })
762
+ },
763
+ alert: (msg) => {
764
+ this.toast({ message: msg })
765
+ },
766
+ openUrl: (url) => {
767
+ this.openUrl(url)
768
+ },
769
+ execCommand: (cmd) => {
770
+ return this.exec(cmd)
771
+ },
772
+ copy: (content, msg) => {
773
+ this.copyToClipboard(content, msg)
774
+ },
775
+ }
776
+ const actionThis = {
777
+ ...task.data,
778
+ ...task.desc,
779
+ actions,
780
+ }
781
+
782
+ try {
783
+ logger.log('actionFunction:', actionFunction)
784
+ const func = new Function(`return ${actionFunction}`)()
785
+ func.apply(actionThis)
786
+ } catch (e) {
787
+ logger.error(e)
788
+ logger.log('action:', actionFunction)
789
+ logger.log('this:', actionThis)
790
+ }
791
+ }
792
+ async performBoardAction(action, task) {
793
+ if (task) task = this.repo.getTask(task.id)
794
+ if (action && action.plugin)
795
+ return await this.pluginManager.performBoardAction(action, task)
796
+ const actions = {
797
+ filter: (filter) => {
798
+ this.setFilter(filter)
799
+ },
800
+ alert: (msg) => {
801
+ this.toast({ message: msg })
802
+ },
803
+ saveFile: ({ file, content }) => {
804
+ this.saveFile(content, file)
805
+ },
806
+ mailto: ({ subject, body, to, cc, bcc }) => {
807
+ const params = []
808
+ if (subject) params.push(`subject=${encodeURIComponent(subject)}`)
809
+ if (body) params.push(`body=${encodeURIComponent(body)}`)
810
+ if (cc) params.push(`cc=${encodeURIComponent(cc)}`)
811
+ if (bcc) params.push(`bcc=${encodeURIComponent(bcc)}`)
812
+ const url = `mailto:${to}?${params.join('&')}`
813
+ logger.log('opening email with:', url)
814
+ this.openUrl(url)
815
+ },
816
+ copy: (content, message) => {
817
+ this.copyToClipboard(content, message || 'Your content has been copied')
818
+ },
819
+ updateCard: (task, content) => {
820
+ this.updateCardContent(task, content)
821
+ },
822
+ }
823
+ const actionFunction = this.boardActions[action.index].action
824
+ const actionThis = { cards: this.lists, ...actions }
825
+ try {
826
+ actionFunction.apply(actionThis)
827
+ } catch (err) {
828
+ logger.error(err)
829
+ logger.log('action:', actionFunction)
830
+ logger.log('this:', actionThis)
831
+ }
832
+ }
833
+
834
+ exec(cmd) {
835
+ return new Promise((resolve, reject) => {
836
+ exec(cmd, (error, stdout, stderr) => {
837
+ if (stderr) logger.warn('cmd stderr:', stderr)
838
+ if (error) return reject(error)
839
+ resolve(stdout)
840
+ })
841
+ })
842
+ }
843
+
844
+ installPlugin({ name, version }) {
845
+ return this.pluginManager.installPlugin({ name, version })
846
+ }
847
+
848
+ uninstallPlugin(name) {
849
+ return this.pluginManager.uninstallPlugin(name)
850
+ }
851
+
852
+ async refresh() {
853
+ await this.repo.refresh()
854
+ await this.pluginManager.reloadPlugins()
855
+ }
856
+
857
+ renderMarkdown(content, filePath) {
858
+ return renderMarkdown(content, filePath || this.path)
859
+ }
860
+
861
+ extractWikilinkTopics(markdown) {
862
+ return extractWikilinkTopics(markdown)
863
+ }
864
+ }