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/file.js ADDED
@@ -0,0 +1,1391 @@
1
+ import _isObject from 'lodash.isobject'
2
+ import _assign from 'lodash.assign'
3
+ import _omit from 'lodash.omit'
4
+ import _get from 'lodash.get'
5
+ import crypto from 'crypto'
6
+ import Emitter from 'events'
7
+ import path from 'path'
8
+ import matter from 'gray-matter'
9
+ import eol from 'eol'
10
+ import { tools } from './tools.js'
11
+ import debug from 'debug'
12
+ import * as chrono from 'chrono-node'
13
+ import { Task } from './task.js'
14
+ import { newCard } from './card.js'
15
+ import { sort } from 'fast-sort'
16
+ import XRegExp from 'xregexp'
17
+ import { getIsoDateWithOffset } from './adapters/date-time.js'
18
+ import {
19
+ FILE_TYPES,
20
+ getTaskContent,
21
+ getRawTask,
22
+ getHashStyleRegex,
23
+ hasTaskInText,
24
+ LIST_NAME_PATTERN,
25
+ LINK_STYLE_REGEX,
26
+ } from './adapters/parsers/task/CardContentParser.js'
27
+ import { appContext } from './context/ApplicationContext.js'
28
+ import { languages } from './languages.js'
29
+ import { logger } from './adapters/logger.js'
30
+
31
+ const log = debug('imdone-core:File')
32
+ const lf = String(eol.lf)
33
+ const lineEnd = String(eol.auto)
34
+ const { hasBlankLines, escapeRegExp } = tools
35
+ const ERRORS = {
36
+ NOT_A_TASK: 'task must be of type Task',
37
+ }
38
+ const CODE_STYLE_END = '((:)(-?[\\d.]+(?:e-?\\d+)?)?)?[ \\t]+(.+)$'
39
+ const CODE_STYLE_PATTERN = '([A-Z]+[A-Z-_]+?)' + CODE_STYLE_END
40
+ const CHECK_STYLE_PATTERN = /^(\s*- \[([x ])\]\s)(.+$)/gm
41
+
42
+ function getTaskId (path, line, text) {
43
+ var shasum = crypto.createHash('sha1')
44
+ const taskElements = { path, line, text: text.trim() }
45
+ shasum.update(JSON.stringify(taskElements))
46
+ return shasum.digest('hex')
47
+ }
48
+
49
+ function getDoneList(config) {
50
+ return config.getDoneList()
51
+ }
52
+
53
+ function getDoingList(config) {
54
+ return config.getDoingList()
55
+ }
56
+
57
+ function getFrontMeta(meta) {
58
+ const metadata = {}
59
+ Object.entries(meta).forEach(([prop, val]) => {
60
+ if (Array.isArray(val)) {
61
+ val.forEach((item) => {
62
+ if (_isObject(item) || Array.isArray(item)) return
63
+ if (!metadata[prop]) metadata[prop] = []
64
+ metadata[prop].push(`${item}`)
65
+ })
66
+ }
67
+ })
68
+ return metadata
69
+ }
70
+
71
+ function trackMarkdownChecklistChanges({task, config, modify}) {
72
+ const doneList = config.getDoneList()
73
+ let beforeTextModified = false
74
+ if (task.beforeText.trim().startsWith('- [ ]')) {
75
+ if (task.list === doneList) {
76
+ if (modify) {
77
+ task.beforeText = task.beforeText.replace('[ ]', '[x]')
78
+ beforeTextModified = true
79
+ } else {
80
+ task.list = config.getDefaultList()
81
+ }
82
+ }
83
+ } else if (task.beforeText.trim().startsWith('- [x]')) {
84
+ if (task.list !== doneList) {
85
+ if (modify) {
86
+ task.beforeText = task.beforeText.replace('[x]', '[ ]')
87
+ beforeTextModified = true
88
+ } else {
89
+ task.list = doneList
90
+ }
91
+ }
92
+ }
93
+
94
+ return beforeTextModified
95
+ }
96
+
97
+ function trimBlankLines (content, isCodeFile) {
98
+ if (isCodeFile) {
99
+ // replace all occurrences of two empty lines in a row with a single empty line
100
+ return content.replace(/\n\s*\n/g, '\n')
101
+ }
102
+ return content.replace(/\n\s*\n\s*\n/g, '\n\n')
103
+ }
104
+
105
+ function padDescription (description = [], beforeText = '') {
106
+ return Task.padDescription(description, beforeText)
107
+ }
108
+
109
+ function bothAreUndefNull(a, b) {
110
+ return isUndefNull(a) && isUndefNull(b)
111
+ }
112
+
113
+ function isUndefNull(val) {
114
+ return val === null || val === undefined
115
+ }
116
+
117
+
118
+
119
+ export class File extends Emitter {
120
+ constructor(opts) {
121
+ super()
122
+ this.project = opts.project
123
+ this.projectContext = appContext().projectContext
124
+ if (_isObject(opts.file) && _isObject(opts.config)) {
125
+ _assign(this, opts.file)
126
+ this.tasks.forEach((task) => {
127
+ task = newCard(task, this.project)
128
+ })
129
+ } else {
130
+ this.repoId = opts.repoId
131
+ this.path = opts.filePath
132
+ this.content = opts.content
133
+ this.modifiedTime = opts.modifiedTime
134
+ this.createdTime = opts.createdTime
135
+ this.languages = opts.languages || languages
136
+ this.modified = false
137
+ this.frontMatter = {
138
+ tags: [],
139
+ context: [],
140
+ meta: {},
141
+ }
142
+ this.tasks = []
143
+ this.isDir = false
144
+ this.lineCount = 0
145
+ this.deleted = false
146
+ }
147
+ }
148
+
149
+ static CODE_STYLE_PATTERN = CODE_STYLE_PATTERN
150
+
151
+ static CHECK_STYLE_PATTERN = CHECK_STYLE_PATTERN
152
+
153
+ static getTaskId = getTaskId
154
+
155
+ static trimBlankLines = trimBlankLines
156
+
157
+ static padDescription = padDescription
158
+
159
+ static isListNameValid(listName) {
160
+ return new XRegExp(LIST_NAME_PATTERN).test(listName)
161
+ }
162
+
163
+ static getUnixContent (content) {
164
+ return content.replace(/\r\n|\r/g, '\n')
165
+ }
166
+
167
+ static isMetaNewLine (config) {
168
+ return config.isMetaNewLine()
169
+ }
170
+
171
+ static isAddCompletedMeta (config) {
172
+ return config.isAddCompletedMeta()
173
+ }
174
+
175
+ static isFile (file) {
176
+ return file instanceof File
177
+ }
178
+
179
+ static isMarkdown (relPath) {
180
+ return /\.md$/i.test(relPath) || path.extname(relPath) === ''
181
+ }
182
+
183
+ static addStarted ({ task, content, config }) {
184
+ if (!task || !config || !config.settings) return content
185
+ if (!config.settings.cards.addStartedMeta) return content
186
+ if (task.meta.started) return content
187
+ if (task.list !== getDoingList(config)) return content
188
+ const now = getIsoDateWithOffset()
189
+ content = task.addToLastCommentInContent(
190
+ content,
191
+ `started${config.getMetaSep()}${now}`,
192
+ File.isMetaNewLine(config)
193
+ )
194
+ const lines = eol.split(content)
195
+ return lines.join(lineEnd)
196
+ }
197
+
198
+ static addCompleted ({ task, content, config }) {
199
+ const doneList = getDoneList(config)
200
+ if (
201
+ !task ||
202
+ !config ||
203
+ !config.settings ||
204
+ !doneList ||
205
+ !this.isAddCompletedMeta(config)
206
+ )
207
+ return content
208
+ if (task.list !== doneList) return content
209
+ if (task.completed || task.meta.completed) return content
210
+ if (content.includes(`completed${config.getMetaSep()}`)) return content
211
+ const now = getIsoDateWithOffset()
212
+ content = task.addToLastCommentInContent(
213
+ content,
214
+ `completed${config.getMetaSep()}${now}`,
215
+ File.isMetaNewLine(config)
216
+ )
217
+ const lines = eol.split(content)
218
+ return lines.join(lineEnd)
219
+ }
220
+
221
+ static parseDueDate (config, text) {
222
+ return File.parseDate(config, text, 'due')
223
+ }
224
+
225
+ static parseRemindDate (config, text) {
226
+ return File.parseDate(config, text, 'remind')
227
+ }
228
+
229
+ static parseDeferDate (config, text) {
230
+ return File.parseDate(config, text, 'defer')
231
+ }
232
+
233
+ static parseDate (config, text, dateType) {
234
+ // TODO Add the ability to set multiple reminders
235
+ // #imdone-1.54.0
236
+ // <!--
237
+ // order:-150
238
+ // -->
239
+ if (text.includes(`${dateType}${config.getMetaSep()}`)) return text
240
+ let dateString
241
+ return text.replace(new RegExp(`(\\s${dateType})\\s.*?\\.`,'gi'), (match, p1) => {
242
+ if (dateString) return match
243
+ try {
244
+ const date = chrono.parseDate(match, new Date(), {forwardDate: true})
245
+ if (!date) return match
246
+ dateString = getIsoDateWithOffset(date)
247
+ return `${p1.toLowerCase()}${config.getMetaSep()}${dateString}`
248
+ } catch (e) {
249
+ logger.log(`Unable to parse ${dateType} date for ${text}`)
250
+ return match
251
+ }
252
+ })
253
+ }
254
+
255
+ removeCompleted ({ task, content, config }) {
256
+ const doneList = getDoneList(config)
257
+ if (doneList === task.list) return content
258
+ if (
259
+ task.meta.completed &&
260
+ task.meta.completed.length > 0
261
+ ) {
262
+ content = this.removeMetaData(
263
+ content,
264
+ 'completed',
265
+ task.meta.completed[0]
266
+ )
267
+ }
268
+ return content
269
+ }
270
+
271
+ trackListChanges ({ task, content, config, modify }) {
272
+ const lists = config.lists
273
+ .filter((list) => !list.filter)
274
+ .map((list) => list.name)
275
+
276
+ let beforeTextModified = false
277
+ if (this.isMarkDownFile()) {
278
+ beforeTextModified = trackMarkdownChecklistChanges({task, config, modify})
279
+
280
+ if (
281
+ _get(config, 'settings.cards.trackChanges') &&
282
+ task.hasListChanged(lists)
283
+ ) {
284
+ const list = task.list
285
+ const now = new Date().toISOString()
286
+ content = task.addToLastCommentInContent(
287
+ content,
288
+ `${list}${config.getMetaSep()}${now}`,
289
+ File.isMetaNewLine(config)
290
+ )
291
+ if (!task.meta[list]) task.meta[list] = []
292
+ task.meta[list].push(now)
293
+ }
294
+ }
295
+
296
+ return { content, beforeTextModified }
297
+ }
298
+
299
+ removeMetaData (content, key, value) {
300
+ return Task.removeMetaData({
301
+ config: this.projectContext.config,
302
+ content,
303
+ key,
304
+ value,
305
+ })
306
+ }
307
+
308
+ transformTask ({config, modify, task}) {
309
+ let content = task.content
310
+
311
+ const trackListChanges = this.trackListChanges({
312
+ task,
313
+ content,
314
+ config,
315
+ modify,
316
+ })
317
+ content = trackListChanges.content
318
+ content = File.parseDueDate(config, content)
319
+ content = File.parseRemindDate(config, content)
320
+ content = File.parseDeferDate(config, content)
321
+ content = File.addStarted({ task, content, config })
322
+ content = File.addCompleted({ task, content, config })
323
+ content = this.removeCompleted({ task, content, config })
324
+ content = task.format(content)
325
+ if (
326
+ content.trim() !== task.content.trim()
327
+ || trackListChanges.beforeTextModified
328
+ || task.orderModified
329
+ ) {
330
+ this.modifyTaskFromContent(task, content, config)
331
+ }
332
+ }
333
+
334
+ transformTasks (config, modify) {
335
+ sort(this.getTasks())
336
+ .desc(u => u.line)
337
+ .forEach(task => this.transformTask({config, modify, task}))
338
+ }
339
+
340
+ // TODO Blank lines should be allowed in code tasks
341
+ // <!--
342
+ // #imdone-1.54.0
343
+ // #feature
344
+ // order:-105
345
+ // -->
346
+ extractCodeStyleTasks (config, pos, content) {
347
+ var self = this
348
+ var codeStyleRegex = new RegExp(CODE_STYLE_PATTERN, 'gm')
349
+ var result
350
+ const inBlockComment = this.isInBlockComment(content)
351
+ const singleLineBlockComment =
352
+ inBlockComment && this.isSingleLineBlockComment(content)
353
+ while ((result = codeStyleRegex.exec(content)) !== null) {
354
+ var posInContent = pos + result.index
355
+ var line = this.getLineNumber(posInContent)
356
+ if (self.getTaskAtLine(line)) continue
357
+ var charBeforeList = this.getContent().substring(
358
+ posInContent - 1,
359
+ posInContent
360
+ )
361
+ if (this.startsWithCommentOrSpace(posInContent) && charBeforeList !== '#') {
362
+ var list = result[1]
363
+ if (!config.includeList(list)) continue
364
+ var rawTask = result[0]
365
+ var text = result[5]
366
+ const linePos = self.getLinePos(line)
367
+ const matchCharsInFront = this.getContent()
368
+ .substring(linePos, posInContent)
369
+ .match(/\S+\s*/)
370
+ const charsInFront = matchCharsInFront ? matchCharsInFront[0].length : 0
371
+ var hasColon =
372
+ result[3] !== undefined
373
+ const taskStartOnLine = inBlockComment
374
+ ? posInContent - charsInFront - linePos
375
+ : pos - linePos
376
+ var task = self.extractTaskWithDescription({
377
+ taskStartOnLine,
378
+ rawTask,
379
+ config,
380
+ list,
381
+ text,
382
+ line,
383
+ type: Task.Types.CODE,
384
+ hasColon,
385
+ content: self.getContent().substring(linePos),
386
+ inBlockComment,
387
+ singleLineBlockComment,
388
+ })
389
+ self.addTask(task)
390
+ self.emit('task.found', task)
391
+ }
392
+ }
393
+ }
394
+
395
+ extractHashStyleTasks (config, pos, content) {
396
+ var hashStyleRegex = getHashStyleRegex(config.tokenPrefix)
397
+ var lang = this.getLang()
398
+ const codePositions = this.getMarkdownCodePositions(content);
399
+ var result
400
+ while ((result = hashStyleRegex.exec(content)) !== null) {
401
+ if (this.isResultInMarkdownCode(codePositions, result.index)) continue;
402
+ var [rawTask, list, orderGroup, order, text] = result
403
+ if (!config.listExists(list)) continue
404
+
405
+ const posInContent = result.index + pos
406
+ if (this.isWithinCodeSpanOrBlock(result.index, content)) continue
407
+
408
+ const line = this.getLineNumber(posInContent)
409
+ if (this.getTaskAtLine(line)) continue
410
+
411
+ if (lang.block) {
412
+ const blockEndPos = text.indexOf(lang.block.end)
413
+ if (blockEndPos > -1) text = text.substring(0, blockEndPos)
414
+ }
415
+ var task = this.extractTaskWithDescription({
416
+ config,
417
+ list,
418
+ text,
419
+ order,
420
+ line,
421
+ type: Task.Types.HASHTAG,
422
+ hasColon: orderGroup && orderGroup.startsWith(':'),
423
+ content: content.substring(result.index),
424
+ pos: posInContent,
425
+ })
426
+ task.taskStartOnLine = pos - this.getLinePos(line)
427
+ task.rawTask = rawTask
428
+ this.addTask(task)
429
+ this.emit('task.found', task)
430
+ }
431
+ }
432
+
433
+ extractLinkStyleTasks (config, pos, content) {
434
+ var self = this
435
+ var linkStyleRegex = new XRegExp(LINK_STYLE_REGEX)
436
+ const log = debug('extractLinkStyleTasks')
437
+ const codePositions = this.getMarkdownCodePositions(content);
438
+ var result
439
+ while ((result = linkStyleRegex.exec(content)) !== null) {
440
+ if (this.isResultInMarkdownCode(codePositions, result.index)) continue;
441
+ let [match, before, rawTask, text, list, colon, order] = result
442
+ const beforeLength = before ? before.length : 0
443
+ const posInContent = result.index + pos + beforeLength
444
+ if (this.isWithinCodeSpanOrBlock(result.index, content)) continue
445
+ var line = this.getLineNumber(posInContent)
446
+ if (self.getTaskAtLine(line)) continue
447
+ if (!config.listExists(list)) continue
448
+ log('*********************************************************')
449
+ log(result)
450
+ log('list:%s text:%s order:%d line:%d', list, text, order, line)
451
+
452
+ var task = self.extractTaskWithDescription({
453
+ config,
454
+ list,
455
+ text,
456
+ order,
457
+ line,
458
+ type: Task.Types.MARKDOWN,
459
+ hasColon: !!colon,
460
+ content: content.substring(result.index),
461
+ pos: posInContent,
462
+ })
463
+ task.rawTask = rawTask
464
+ self.addTask(task)
465
+ self.emit('task.found', task)
466
+ }
467
+ }
468
+
469
+ extractCheckboxTasks (config, pos, content) {
470
+ if (!this.isMarkDownFile()) return
471
+ if (!config.isAddCheckBoxTasks()) return
472
+
473
+ const codePositions = this.getMarkdownCodePositions(content);
474
+ const type = Task.Types[config.getNewCardSyntax()]
475
+ const checkStyleRegex = new RegExp(CHECK_STYLE_PATTERN)
476
+ let result
477
+ let tasks = []
478
+ const hasTaskAtLine = (line) => {
479
+ return (
480
+ this.getTaskAtLine(line) ||
481
+ tasks.find((task) => {
482
+ return line >= task.line && line <= task.lastLine
483
+ })
484
+ )
485
+ }
486
+ while ((result = checkStyleRegex.exec(content)) !== null) {
487
+ if (this.isResultInMarkdownCode(codePositions, result.index)) continue;
488
+ let [match, before, checked, text] = result
489
+ const list =
490
+ checked.trim() === 'x' ? config.getDoneList() : config.getDefaultList()
491
+ const beforeLength = before.length
492
+ const posInContent = result.index + pos + beforeLength
493
+ var line = this.getLineNumber(posInContent)
494
+ if (hasTaskAtLine(line)) continue
495
+ var task = this.extractTaskWithDescription({
496
+ config,
497
+ list,
498
+ text,
499
+ line,
500
+ type,
501
+ hasColon: true,
502
+ content: content.substring(result.index),
503
+ pos: posInContent,
504
+ })
505
+ task.rawTask = getRawTask({tokenPrefix: config.tokenPrefix, orderMeta: config.orderMeta, list, text, type})
506
+ tasks.push(task)
507
+ }
508
+ if (tasks) {
509
+ const hasTaskAtLine = (line) => {
510
+ return (
511
+ this.getTaskAtLine(line) ||
512
+ tasks.find((task) => {
513
+ return line > task.line && line <= task.lastLine
514
+ })
515
+ )
516
+ }
517
+ tasks = tasks.filter((task) => {
518
+ return !hasTaskAtLine(task.line)
519
+ })
520
+ if (!tasks) return
521
+ let newContent = content
522
+ tasks.reverse().forEach((task) => {
523
+ this.addTask(task)
524
+ const beforeTaskContent = content.substring(0, task.pos)
525
+ const afterTaskContent = newContent.substring(task.pos + task.text.length)
526
+ newContent = beforeTaskContent + task.rawTask + afterTaskContent
527
+ this.emit('task.found', task)
528
+ })
529
+
530
+ if (this.content === newContent || !newContent) return
531
+
532
+ this.content = newContent
533
+
534
+ this.setModified(true)
535
+ logger.log('extractCheckboxTasks setModified true for file:', this.path)
536
+ }
537
+ }
538
+ extractTasks (config) {
539
+ this.tasks = []
540
+ if (this.isMarkDownFile()) {
541
+ const data = this.parseFrontMatter(config)
542
+ if (data.imdone_ignore || data['kanban-plugin']) return this
543
+ }
544
+ if (this.isCodeFile()) {
545
+ this.extractTasksInCodeFile(config)
546
+ } else {
547
+ this.extractTasksInNonCodeFile(config)
548
+ }
549
+ return this
550
+ }
551
+
552
+ extractAndTransformTasks (config) {
553
+ this.extractTasks(config)
554
+ this.transformTasks(config)
555
+ }
556
+
557
+ // TODO Extract functions should be moved to another file
558
+ // Only pass the needed config values to the functions
559
+ // #imdone-1.54.0
560
+ // <!--
561
+ // order:40
562
+ // -->
563
+ extractTasksInCodeFile (config) {
564
+ var self = this
565
+ var commentRegex = this.getCodeCommentRegex()
566
+ var result
567
+ while ((result = commentRegex.exec(self.getContent())) !== null) {
568
+ var comment = result[0]
569
+ var commentStart = result.index
570
+ self.extractCodeStyleTasks(config, commentStart, comment)
571
+ }
572
+ }
573
+
574
+ extractTasksInNonCodeFile (config) {
575
+ this.extractHashStyleTasks(config, 0, this.getContent())
576
+ this.extractLinkStyleTasks(config, 0, this.getContent())
577
+ this.extractCheckboxTasks(config, 0, this.getContent())
578
+ }
579
+
580
+ deleteBlankLine (lineNo) {
581
+ var startOfLine = this.getLinePos(lineNo)
582
+ if (this.isMarkDownFile()) startOfLine = startOfLine - 1
583
+ var startOfNextLine = this.getLinePos(lineNo + 1)
584
+ if (startOfNextLine < 0) return
585
+ var content = this.content
586
+ var lineContent = content.substring(startOfLine, startOfNextLine)
587
+ const trimmedLine = lineContent.trim()
588
+ if (
589
+ trimmedLine === '' ||
590
+ (this.isMarkDownFile() && /^(-|\*+|\#+)$/.test(trimmedLine))
591
+ ) {
592
+ var start = content.substring(0, startOfLine)
593
+ var end = content.substring(startOfNextLine)
594
+ this.setContent(start + end)
595
+ }
596
+ }
597
+
598
+ deleteTaskContent (beforeContent, afterContent) {
599
+ const lang = this.getLang()
600
+ if (this.isCodeFile() && lang.block) {
601
+ const beforeContentTrim = beforeContent.trim()
602
+ const blockStartTrimPos = beforeContentTrim.lastIndexOf(lang.block.start)
603
+ if (
604
+ blockStartTrimPos > -1 &&
605
+ blockStartTrimPos === beforeContentTrim.length - lang.block.start.length
606
+ ) {
607
+ const blockStartPos = beforeContent.lastIndexOf(lang.block.start)
608
+ const blockEndPos =
609
+ afterContent.indexOf(lang.block.end) + lang.block.end.length
610
+ beforeContent = beforeContent.substring(0, blockStartPos)
611
+ afterContent = afterContent.substring(blockEndPos)
612
+ }
613
+ }
614
+
615
+ this.setContent(beforeContent + afterContent)
616
+ }
617
+
618
+ deleteDescription (task, config) {
619
+ if (task.singleLineBlockComment) return
620
+ task.description = []
621
+ this.modifyDescription(task, config)
622
+ }
623
+
624
+ // TODO This isn't deleting all the newlines at the end of each line in code files
625
+ // <!--
626
+ // #bug
627
+ // #imdone-1.54.0
628
+ // order:-165
629
+ // -->
630
+ deleteCodeOrHashTask (re, task, config) {
631
+ var log = debug('delete-task:deleteCodeOrHashTask')
632
+ var self = this
633
+ var line = task.getLine()
634
+ re = new XRegExp(re)
635
+ re.lastIndex = this.getLinePos(line)
636
+ var result = re.exec(this.getContent())
637
+ if (result) {
638
+ log('Got result: %j', result)
639
+ var lang = self.getLang()
640
+ var text = result[task.type === Task.Types.HASHTAG ? 4 : 5]
641
+ var taskText = this.trimCommentBlockEnd(text)
642
+ if (this.tasksMatch(config, task, line, taskText)) {
643
+ var index = result.index
644
+ var afterStart = re.lastIndex
645
+ if (index < 0) index = 0
646
+ if (self.isCodeFile()) {
647
+ var commentStart = this.getLinePos(line) + task.taskStartOnLine
648
+ var commentPrefix = this.getContent().substring(commentStart, index)
649
+ var symbolRe = new RegExp(escapeRegExp(lang.symbol) + '\\s*')
650
+ if (symbolRe.test(commentPrefix)) index = commentStart
651
+ else if (lang && lang.block && lang.block.end) {
652
+ var blockEndRe = new RegExp(escapeRegExp(lang.block.end) + '\\s*$')
653
+ var match = blockEndRe.exec(task.rawTask)
654
+ if (match) afterStart -= match[0].length
655
+ }
656
+ }
657
+ var beforeContent = this.getContent().substring(0, index)
658
+ var afterContent = this.getContent().substring(afterStart)
659
+ this.deleteTaskContent(beforeContent, afterContent)
660
+ this.deleteBlankLine(line)
661
+ this.setModified(true)
662
+ // task.type = Task.Types.HASHTAG;
663
+ this.emit('task.deleted', task)
664
+ this.emit('file.modified', self)
665
+ }
666
+ }
667
+
668
+ return this
669
+ }
670
+
671
+ deleteTask (task, config) {
672
+ var self = this
673
+ if (this.isCodeFile()) {
674
+ this.deleteDescription(task, config)
675
+ if (task.type === Task.Types.CODE) {
676
+ this.deleteCodeOrHashTask(
677
+ new RegExp(CODE_STYLE_PATTERN, 'gm'),
678
+ task,
679
+ config
680
+ )
681
+ } else if (task.type === Task.Types.HASHTAG) {
682
+ this.deleteCodeOrHashTask(getHashStyleRegex(config.tokenPrefix), task, config)
683
+ }
684
+ } else {
685
+ const fileContent = this.getContentLines()
686
+ const newContent = [
687
+ ...fileContent.slice(0, task.line - 1),
688
+ ...fileContent.slice(task.lastLine),
689
+ ]
690
+ this.setContent(newContent.join(lineEnd))
691
+ this.setModified(true)
692
+ this.emit('task.deleted', task)
693
+ this.emit('file.modified', self)
694
+ }
695
+ }
696
+
697
+ modifyTask (task, config, noEmit) {
698
+ task.orderModified = true
699
+ task.updateOrderMeta(config)
700
+ if (task.type === Task.Types.CODE) {
701
+ this.modifyCodeOrHashTask(
702
+ new RegExp(CODE_STYLE_PATTERN, 'gm'),
703
+ task,
704
+ config,
705
+ noEmit
706
+ )
707
+ } else {
708
+ task.updateContent()
709
+ const fileContent = this.getContentLines()
710
+ let text = getRawTask({
711
+ tokenPrefix: config.tokenPrefix,
712
+ orderMeta: config.orderMeta,
713
+ beforeText: task.beforeText,
714
+ hasColon: task.hasColon,
715
+ list: task.list,
716
+ order: task.order,
717
+ text: task.text,
718
+ type: task.type,
719
+ })
720
+ const linesBefore = fileContent.slice(0, task.line - 1)
721
+ if (this.isCodeFile()) {
722
+ const linesBeforeLength = linesBefore.join(lineEnd).length
723
+ const charsBefore = task.pos - (linesBeforeLength + 1)
724
+ if (charsBefore > 0) {
725
+ text = fileContent[task.line -1].substring(0,charsBefore) + text
726
+ }
727
+ }
728
+
729
+ const linesAfter = fileContent.slice(task.line)
730
+ const newContent = [
731
+ ...linesBefore,
732
+ text,
733
+ ...linesAfter,
734
+ ].join(lineEnd)
735
+
736
+ const contentBeforeUpdate = this.getContent()
737
+ this.setContent(newContent)
738
+ this.modifyDescription(task, config)
739
+ if (contentBeforeUpdate.trim() == this.getContent().trim()) return
740
+
741
+ this.setModified(true)
742
+ if (!noEmit) {
743
+ this.emit('task.modified', task)
744
+ this.emit('file.modified', this)
745
+ }
746
+ }
747
+ }
748
+
749
+ modifyCodeOrHashTask (re, task, config, noEmit) {
750
+ log('In modifyCodeOrHashTask:%j', task)
751
+ log('--------------------------------')
752
+ log(`modifying task: ${task.rawTask}`)
753
+ log(`line: ${task.line}`)
754
+ var self = this
755
+ var line = task.getLine()
756
+ re = new RegExp(re)
757
+ var lang = this.getLang()
758
+ var linePos = (re.lastIndex = this.getLinePos(line))
759
+ var nextLinePos = this.getLinePos(line + 1)
760
+ var lineContent = this.getContent().substring(linePos, nextLinePos)
761
+ if (lineContent.indexOf(lang.symbol) > -1) {
762
+ re.lastIndex = this.getContent().indexOf(lang.symbol, linePos)
763
+ }
764
+ if (lang.block && lineContent.indexOf(lang.block.start) > -1) {
765
+ re.lastIndex = this.getContent().indexOf(lang.block.start, linePos)
766
+ }
767
+
768
+ var result
769
+ while ((result = re.exec(this.getContent())) !== null) {
770
+ log('Got result: %j', result)
771
+ var text = result[task.type === Task.Types.HASHTAG ? 3 : 5]
772
+ var taskText = this.trimCommentBlockEnd(text)
773
+ if (this.tasksMatch(config, task, line, taskText)) {
774
+ if (task.updatedText) task.text = task.updatedText
775
+ var index = result.index
776
+ const pos = this.isCodeFile() ? index : this.getLinePos(line)
777
+ if (index < 0) index = 0
778
+ var beforeContent = this.getContent().substring(0, pos)
779
+ var afterContent = this.getContent().substring(re.lastIndex)
780
+ if (task.inBlockComment) {
781
+ var blockEnd = text.indexOf(lang.block.end)
782
+ if (blockEnd > -1) {
783
+ const desc =
784
+ task.description.length > 0
785
+ ? `${lineEnd}${task.description.join(lineEnd)}`
786
+ : ''
787
+ text = task.text + desc + text.substring(blockEnd)
788
+ } else text = task.text
789
+ } else text = task.text
790
+ if (/[a-z]/.test(task.list)) task.type = Task.Types.HASHTAG
791
+ const tokenPrefix = task.type === Task.Types.HASHTAG ? config.tokenPrefix : ''
792
+ var taskContent = getRawTask({tokenPrefix, orderMeta: config.orderMeta, list: task.list, order: task.order, text, type: task.type})
793
+ task.line = this.getLineNumber(index)
794
+ task.id = getTaskId(self.getPath(), task.line, task.text)
795
+ this.setContent(beforeContent + taskContent + afterContent)
796
+ if (!task.singleLineBlockComment) {
797
+ this.modifyDescription(task, config)
798
+ }
799
+ this.setModified(true)
800
+ // logger.log('modifyCodeOrHashTask setModified true for file:', this.path)
801
+ if (!noEmit) {
802
+ this.emit('task.modified', task)
803
+ this.emit('file.modified', self)
804
+ }
805
+ return this
806
+ }
807
+ }
808
+
809
+ return this
810
+ }
811
+
812
+ modifyTaskFromHtml (task, html) {
813
+ const checks = task.getChecksFromHtml(html)
814
+ const re = /- \[[\sx]{1}\]/g
815
+ const descStart = this.getLinePos(task.line + 1)
816
+ const descEnd = this.getLinePos(task.line + 1 + task.description.length)
817
+ let descContent =
818
+ descEnd > 0
819
+ ? this.getContent().substring(descStart, descEnd)
820
+ : this.getContent().substring(descStart)
821
+ const beforeDescContent = this.getContent().substring(0, descStart)
822
+ const afterDescContent =
823
+ descEnd > 0 ? this.getContent().substring(descEnd) : ''
824
+ let i = 0
825
+ descContent = descContent.replace(re, (match) => {
826
+ const check = checks[i]
827
+ i++
828
+ if (check === undefined) return match
829
+ let char = check ? 'x' : ' '
830
+ return `- [${char}]`
831
+ })
832
+
833
+ this.setContent(beforeDescContent + descContent + afterDescContent)
834
+ this.setModified(true)
835
+ logger.log('modifyTaskFromHtml setModified true for file:', this.path)
836
+ }
837
+
838
+ modifyTaskFromContent (task, content, config) {
839
+ content = trimBlankLines(content, this.isCodeFile())
840
+ task.updateFromContent(content)
841
+ this.modifyTask(task, config)
842
+ }
843
+
844
+ modifyDescription (task, config) {
845
+ // TODO This should be split out by type (code, markdown, etc)
846
+ // #urgent
847
+ // <!-- order:-70 -->
848
+ const taskStart = this.getLinePos(task.line)
849
+ const descStart = this.getLinePos(task.line + 1)
850
+ const contentWithTask = this.getContent().substring(taskStart)
851
+ let {
852
+ rawTaskContentLines,
853
+ taskContentLines,
854
+ isWrappedWithCardTag,
855
+ trailingBlankLines
856
+ } =
857
+ this.getTaskContent({
858
+ config,
859
+ content: contentWithTask,
860
+ inBlockComment: task.inBlockComment,
861
+ beforeText: task.beforeText,
862
+ })
863
+
864
+ let beforeDescContent = this.getContent().substring(0, descStart)
865
+
866
+ if (descStart === this.getContent().length && !beforeDescContent.endsWith(lf))
867
+ beforeDescContent += lineEnd
868
+
869
+ let content = this.getContent().substring(descStart)
870
+ let spaces = ''
871
+ if (this.isCodeFile() && !this.isMarkDownFile()) {
872
+ spaces = contentWithTask.substring(0, task.commentStartOnLine)
873
+ spaces = spaces.replace(/\S/g, ' ')
874
+ }
875
+ const descriptionStartsWith = task.descriptionStartsWith
876
+ ? `${task.descriptionStartsWith} `
877
+ : ''
878
+ let description = task.description.map(
879
+ (desc) => `${spaces}${descriptionStartsWith}${desc}`
880
+ )
881
+ // BACKLOG Allow this to pad for code files as well
882
+ // <!--
883
+ // order:-620
884
+ // -->
885
+ description = File.padDescription(description, task.beforeText).join(lineEnd)
886
+ if (taskContentLines.length === 0 && description.length > 0) {
887
+ beforeDescContent += description + lineEnd
888
+ } else {
889
+ let rawTaskContent = rawTaskContentLines.join(lineEnd)
890
+ // Add card terminator to description
891
+ if (
892
+ this.isMarkDownFile() &&
893
+ description.length > 0 &&
894
+ (hasBlankLines(description) || isWrappedWithCardTag || trailingBlankLines)
895
+ ) {
896
+ task.isWrappedWithCardTag = isWrappedWithCardTag // FIXME Find out where this is used and stop using it
897
+ // <!-- order:-30 -->
898
+ const blankLinesToAdd = isWrappedWithCardTag ? 2 : trailingBlankLines
899
+ description = `${description}${lineEnd.repeat(blankLinesToAdd)}`
900
+ }
901
+
902
+ // Handle code file tasks
903
+ if (
904
+ task.singleLineBlockComment &&
905
+ rawTaskContent.length > 0 &&
906
+ description.length === 0
907
+ ) {
908
+ description = lineEnd + description
909
+ }
910
+ content = content.replace(rawTaskContent, description)
911
+ }
912
+
913
+ task.lastLine = task.line + description.split(lineEnd).length
914
+ this.setContent(beforeDescContent + content)
915
+ }
916
+
917
+ extractTaskWithDescription ({
918
+ taskStartOnLine,
919
+ rawTask,
920
+ config,
921
+ list,
922
+ text,
923
+ order,
924
+ line,
925
+ type,
926
+ hasColon,
927
+ content,
928
+ inBlockComment,
929
+ singleLineBlockComment,
930
+ pos,
931
+ }) {
932
+ var self = this
933
+ const lang = this.getLang()
934
+ let description = []
935
+ // BACKLOG ## add beforeText for code files
936
+ // <!--
937
+ // order:-630
938
+ // -->
939
+ const beforeText = this.getBeforeText(line, pos)
940
+ let { rawTaskContentLines, taskContentLines, isWrappedWithCardTag } = this.getTaskContent({
941
+ config,
942
+ content,
943
+ inBlockComment,
944
+ beforeText,
945
+ })
946
+
947
+ if (!singleLineBlockComment) {
948
+ description = this.isCodeFile()
949
+ ? taskContentLines.map((line) => this.trimCommentChars(line))
950
+ : structuredClone(taskContentLines)
951
+ }
952
+ description = Task.trimDescription(description, beforeText)
953
+ const descriptionStartsWith = this.getDescriptionChars(inBlockComment)
954
+ const frontMatter = this.frontMatter
955
+ text = this.trimCommentBlockEnd(text)
956
+ let commentStartOnLine =
957
+ content.search(/\w/) - (descriptionStartsWith.length + 1)
958
+ if (descriptionStartsWith === lang.symbol)
959
+ commentStartOnLine = taskStartOnLine
960
+
961
+ var task = newCard(
962
+ {
963
+ pos,
964
+ frontMatter,
965
+ inBlockComment,
966
+ singleLineBlockComment,
967
+ rawTask,
968
+ text,
969
+ list,
970
+ rawTaskContentLines,
971
+ description,
972
+ descriptionStartsWith,
973
+ taskStartOnLine,
974
+ commentStartOnLine,
975
+ hasColon,
976
+ order: self.projectContext.getOrder(list, order),
977
+ line,
978
+ id: getTaskId(self.getPath(), line, text),
979
+ repoId: self.getRepoId(),
980
+ source: self.getSource(),
981
+ type,
982
+ beforeText,
983
+ isWrappedWithCardTag,
984
+ },
985
+ this.project
986
+ )
987
+
988
+ task.init()
989
+ if (!config.keepEmptyPriority) task.updateOrderMeta(config, task.format(task.descriptionString))
990
+ task.orderModified = task.order + "" !== order + "" && !bothAreUndefNull(task.order, order)
991
+
992
+ return task
993
+ }
994
+
995
+ tasksMatch (config, task, line, taskText) {
996
+ return (
997
+ task.id == getTaskId(this.getPath(), line, taskText) ||
998
+ (task.meta.id &&
999
+ Task.getMetaData(config, taskText).id &&
1000
+ task.meta.id[0] === Task.getMetaData(config, taskText).id[0])
1001
+ )
1002
+ }
1003
+
1004
+ getTaskAtLine (line) {
1005
+ return this.getTasks().find((task) => {
1006
+ return line >= task.line && line <= task.lastLine
1007
+ })
1008
+ }
1009
+
1010
+ getLinePos (lineNo) {
1011
+ return Task.getLinePos(this.content, lineNo)
1012
+ }
1013
+
1014
+ getLineNumber (pos, content = this.content) {
1015
+ return Task.getLineNumber(content, pos)
1016
+ }
1017
+
1018
+ toJSON () {
1019
+ return _omit(this, ['domain', '_events', '_maxListeners'])
1020
+ }
1021
+
1022
+ getRepoId () {
1023
+ return this.repoId
1024
+ }
1025
+
1026
+ getPath () {
1027
+ return this.path
1028
+ }
1029
+
1030
+ getFullPath () {
1031
+ return path.join(this.repoId, this.path)
1032
+ }
1033
+
1034
+ getId () {
1035
+ return this.getPath()
1036
+ }
1037
+
1038
+ reset () {
1039
+ this.previousContent = this.content
1040
+ this.content = null
1041
+ this.modified = false
1042
+ return this
1043
+ }
1044
+
1045
+ rollback () {
1046
+ this.content = this.previousContent
1047
+ return this
1048
+ }
1049
+
1050
+ setContent (content) {
1051
+ this.content = content
1052
+ return this
1053
+ }
1054
+
1055
+ setContentFromFile (content) {
1056
+ this.content = eol.auto(content || '')
1057
+ return this
1058
+ }
1059
+
1060
+ getContent () {
1061
+ return this.content
1062
+ }
1063
+
1064
+ getContentForFile () {
1065
+ return eol.auto(this.content || '')
1066
+ }
1067
+
1068
+ getContentLines () {
1069
+ return eol.split(this.getContentForFile())
1070
+ }
1071
+
1072
+ setModifiedTime (modifiedTime) {
1073
+ this.modifiedTime = modifiedTime
1074
+ return this
1075
+ }
1076
+
1077
+ setCreatedTime (createdTime) {
1078
+ this.createdTime = createdTime
1079
+ return this
1080
+ }
1081
+
1082
+ getCreatedTime () {
1083
+ return this.createdTime
1084
+ }
1085
+
1086
+ getModifiedTime () {
1087
+ return this.modifiedTime
1088
+ }
1089
+
1090
+ setModified (modified) {
1091
+ this.modified = modified
1092
+ return this
1093
+ }
1094
+
1095
+ isModified () {
1096
+ return this.modified
1097
+ }
1098
+
1099
+ getType () {
1100
+ return this.constructor.name
1101
+ }
1102
+
1103
+ getTasks () {
1104
+ return this.tasks
1105
+ }
1106
+
1107
+ getTask (id) {
1108
+ return (
1109
+ this.getTasks().find((task) => id === task.id) ||
1110
+ this.getTasks().find(
1111
+ (task) => task.meta && task.meta.id && task.meta.id[0] === id.toString()
1112
+ )
1113
+ )
1114
+ }
1115
+
1116
+ addTask (task) {
1117
+ if (!(task instanceof Task)) throw new Error(ERRORS.NOT_A_TASK)
1118
+ if (!Array.isArray(this.tasks)) this.tasks = []
1119
+ var index = this.tasks.findIndex(({ id }) => task.id === id)
1120
+ log(
1121
+ 'Adding task with text:%s in list:%s with order:%d at index %d',
1122
+ task.text,
1123
+ task.list,
1124
+ task.order,
1125
+ index
1126
+ )
1127
+ if (index > -1) {
1128
+ this.tasks[index] = task
1129
+ } else {
1130
+ this.tasks.push(task)
1131
+ }
1132
+ return this
1133
+ }
1134
+
1135
+ removeTask (task) {
1136
+ if (!(task instanceof Task)) throw new Error(ERRORS.NOT_A_TASK)
1137
+ if (!Array.isArray(this.tasks)) this.tasks = []
1138
+ var index = this.tasks.findIndex(({ id }) => task.id === id)
1139
+ if (index > -1) {
1140
+ this.tasks.splice(index, 1)
1141
+ }
1142
+ }
1143
+
1144
+ isMarkDownFile () {
1145
+ return /^md|markdown$/i.test(this.getExt())
1146
+ }
1147
+
1148
+ getLang () {
1149
+ var lang = this.languages[path.extname(this.path)]
1150
+ return lang || { name: 'text', symbol: '' }
1151
+ }
1152
+
1153
+ getExt () {
1154
+ return path.extname(this.path).substring(1).toLowerCase()
1155
+ }
1156
+
1157
+ isCodeFile () {
1158
+ var symbol = this.getLang().symbol
1159
+ return symbol && symbol !== ''
1160
+ }
1161
+
1162
+ getBeforeText (line, pos) {
1163
+ return this.isCodeFile()
1164
+ ? null
1165
+ : this.content.substring(this.getLinePos(line), pos)
1166
+ }
1167
+
1168
+ getDescriptionChars (inBlockComment) {
1169
+ if (!this.isCodeFile()) return ''
1170
+ const lang = this.getLang()
1171
+ if (inBlockComment && lang.block && lang.block.ignore) return lang.block.ignore
1172
+ return lang.symbol
1173
+ }
1174
+
1175
+ getTaskContent ({
1176
+ config,
1177
+ content,
1178
+ inBlockComment,
1179
+ beforeText,
1180
+ }) {
1181
+ const isMarkDownFile = this.isMarkDownFile()
1182
+ const isCodeFile = this.isCodeFile()
1183
+ const lang = this.getLang()
1184
+ const fileType = isMarkDownFile ? FILE_TYPES.MARKDOWN : isCodeFile ? FILE_TYPES.CODE : undefined
1185
+ return getTaskContent({
1186
+ config,
1187
+ content,
1188
+ inBlockComment,
1189
+ beforeText,
1190
+ lang,
1191
+ fileType,
1192
+ })
1193
+ }
1194
+
1195
+ isCheckBoxTask (config, line, beforeText) {
1196
+ if (!config.isAddCheckBoxTasks()) return
1197
+
1198
+ const beforeTextCheckData = Task.getCheckedData(beforeText)
1199
+ if (!beforeTextCheckData) return
1200
+
1201
+ const lineCheckData = Task.getCheckedData(line)
1202
+ if (!lineCheckData) return
1203
+
1204
+ return lineCheckData.pad <= beforeTextCheckData.pad
1205
+ }
1206
+
1207
+ getCodeCommentRegex () {
1208
+ // #BACKLOG:-460 Allow languages to have multiple block comment styles, like html gh:13 id:5
1209
+ var lang = this.getLang()
1210
+ var symbol = lang.symbol
1211
+ var reString = '(?<!["\'`])' + escapeRegExp(symbol) + '[^{].*$'
1212
+
1213
+ if (lang.block) {
1214
+ var start = escapeRegExp(lang.block.start)
1215
+ var end = escapeRegExp(lang.block.end)
1216
+ //
1217
+ reString = reString + '|(?<!["\'`])' + start + '(.|[\\r\\n])*?' + end
1218
+ }
1219
+
1220
+ return new RegExp(reString, 'gmi')
1221
+ }
1222
+
1223
+ trimCommentStart (text) {
1224
+ if (this.isCodeFile() && this.getLang().symbol) {
1225
+ var start = escapeRegExp(this.getLang().symbol)
1226
+ var startPattern = `^\\s*${start}\\s?`
1227
+ return text.replace(new RegExp(startPattern), '')
1228
+ }
1229
+ return text
1230
+ }
1231
+
1232
+ trimCommentBlockEnd (text) {
1233
+ if (this.isCodeFile() && this.getLang().block) {
1234
+ var blockEnd = escapeRegExp(this.getLang().block.end)
1235
+ var endPattern = `\\s?${blockEnd}.*$`
1236
+ return text.replace(new RegExp(endPattern), '')
1237
+ }
1238
+ return text
1239
+ }
1240
+
1241
+ trimCommentBlockStart (text) {
1242
+ if (this.isCodeFile() && this.getLang().block) {
1243
+ var blockStart = escapeRegExp(this.getLang().block.start)
1244
+ var startPattern = `^\\s*${blockStart}\\s?`
1245
+ return text.replace(new RegExp(startPattern), '')
1246
+ }
1247
+ return text
1248
+ }
1249
+
1250
+ trimCommentBlockIgnore (text) {
1251
+ if (
1252
+ this.isCodeFile() &&
1253
+ this.getLang().block &&
1254
+ this.getLang().block.ignore
1255
+ ) {
1256
+ var blockIgnore = escapeRegExp(this.getLang().block.ignore)
1257
+ var ignorePattern = `^\\s*${blockIgnore}\\s?`
1258
+ return text.replace(new RegExp(ignorePattern), '')
1259
+ }
1260
+ return text
1261
+ }
1262
+
1263
+ trimCommentChars (text) {
1264
+ let newText = this.trimCommentStart(text)
1265
+ if (text === newText) newText = this.trimCommentBlockEnd(text)
1266
+ if (text === newText) newText = this.trimCommentBlockStart(text)
1267
+ if (text === newText) newText = this.trimCommentBlockIgnore(text)
1268
+ return newText
1269
+ }
1270
+
1271
+ hasCodeStyleTask (config, text) {
1272
+ if (!this.isCodeFile()) return false
1273
+ let result = new RegExp(CODE_STYLE_PATTERN).exec(text)
1274
+ if (!result) return false
1275
+ return config.includeList(result[1])
1276
+ }
1277
+
1278
+ hasTaskInText (config, text) {
1279
+ return hasTaskInText(config, text, this.isCodeFile())
1280
+ }
1281
+
1282
+ isInBlockComment (content) {
1283
+ if (!this.isCodeFile()) return false
1284
+ const lang = this.getLang()
1285
+ return (
1286
+ lang.block &&
1287
+ lang.block.start &&
1288
+ content.trim().startsWith(lang.block.start)
1289
+ )
1290
+ }
1291
+
1292
+ startsWithCommentOrSpace (pos) {
1293
+ var lang = this.getLang()
1294
+ var symbol = lang.symbol
1295
+ var blockStart = lang.block && lang.block.start
1296
+ if (symbol === this.getContent().substring(pos - symbol.length, pos))
1297
+ return true
1298
+ if (
1299
+ blockStart &&
1300
+ blockStart === this.getContent().substring(pos - blockStart.length, pos)
1301
+ )
1302
+ return true
1303
+ if (this.getContent().substring(pos - 1, pos) === ' ') return true
1304
+ return false
1305
+ }
1306
+
1307
+ isSingleLineBlockComment (content) {
1308
+ return eol.split(content).length === 1
1309
+ }
1310
+
1311
+ parseFrontMatter (config) {
1312
+ this.frontMatter = {
1313
+ tags: [],
1314
+ context: [],
1315
+ meta: {},
1316
+ }
1317
+
1318
+ if (config.ignoreFrontMatter) return {}
1319
+ try {
1320
+ const { data, isEmpty } = matter(this.getContent())
1321
+ if (!isEmpty) {
1322
+ this.frontMatter = { props: {}, ...data, ...this.frontMatter }
1323
+ if (data.meta) this.frontMatter.meta = getFrontMeta(data.meta)
1324
+ if (data.context) {
1325
+ this.frontMatter.context = Array.isArray(data.context)
1326
+ ? data.context.map((context) => context.trim())
1327
+ : data.context
1328
+ .toString()
1329
+ .split(',')
1330
+ .map((context) => context.trim())
1331
+ }
1332
+ if (data.tags && !config.ignoreFrontMatterTags) {
1333
+ this.frontMatter.tags = Array.isArray(data.tags)
1334
+ ? data.tags.map((tag) => tag.trim())
1335
+ : data.tags
1336
+ .toString()
1337
+ .split(',')
1338
+ .map((tag) => tag.trim())
1339
+ }
1340
+ return data
1341
+ }
1342
+ } catch (err) {
1343
+ logger.error(`Error processing front-matter in:${this.path}`, err)
1344
+ }
1345
+ return {}
1346
+ }
1347
+
1348
+ getSource () {
1349
+ var self = this
1350
+ return {
1351
+ path: self.getPath(),
1352
+ id: self.getId(),
1353
+ repoId: self.getRepoId(),
1354
+ type: self.getType(),
1355
+ ext: self.getExt(),
1356
+ lang: self.getLang().name,
1357
+ modified: self.isModified(),
1358
+ modifiedTime: self.getModifiedTime(),
1359
+ createdTime: self.getCreatedTime(),
1360
+ }
1361
+ }
1362
+
1363
+ isWithinCodeSpanOrBlock(pos, content) {
1364
+ const positions = Task.getMarkdownCodePositions(content)
1365
+ return Task.isResultInMarkdownCode(positions, pos)
1366
+ }
1367
+
1368
+ getMarkdownCodePositions (text) {
1369
+ const positions = [];
1370
+ const codeBlockRe = /```[\s\S]*?```/g;
1371
+ const inlineCodeRe = /`[^`]*`/g;
1372
+
1373
+ let match;
1374
+
1375
+ // Find code blocks
1376
+ while ((match = codeBlockRe.exec(text)) !== null) {
1377
+ positions.push({ start: match.index, end: match.index + match[0].length });
1378
+ }
1379
+
1380
+ // Find inline code spans
1381
+ while ((match = inlineCodeRe.exec(text)) !== null) {
1382
+ positions.push({ start: match.index, end: match.index + match[0].length });
1383
+ }
1384
+
1385
+ return positions;
1386
+ };
1387
+
1388
+ isResultInMarkdownCode (positions, index) {
1389
+ return positions.some(pos => index >= pos.start && index < pos.end);
1390
+ };
1391
+ }