Welcome to mirror list, hosted at ThFree Co, Russian Federation.

utils.js « show « issues « javascripts « assets « app - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 7742a01583677569a6c8137bbe6865df875cd1a7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import { TITLE_LENGTH_MAX } from '~/issues/constants';
import { COLON, HYPHEN, NEWLINE } from '~/lib/utils/text_utility';
import { __ } from '~/locale';

/**
 * Returns the start and end `sourcepos` rows, converted to zero-based numbering.
 *
 * @param {String} sourcepos Source position in format `23:3-23:14`
 * @returns {Array<Number>} Start and end `sourcepos` rows, zero-based numbered
 */
const getSourceposRows = (sourcepos) => {
  const [startRange, endRange] = sourcepos.split(HYPHEN);
  const [startRow] = startRange.split(COLON);
  const [endRow] = endRange.split(COLON);
  return [startRow - 1, endRow - 1];
};

/**
 * Given a `ul` or `ol` element containing a new sort order, this function returns
 * an array of this new order which is derived from its list items' sourcepos values.
 *
 * @param {HTMLElement} list A `ul` or `ol` element containing a new sort order
 * @returns {Array<Number>} A numerical array representing the new order of the list.
 * The numbers represent the rows of the original markdown source.
 */
const getNewSourcePositions = (list) => {
  const newSourcePositions = [];

  Array.from(list.children).forEach((listItem) => {
    const [start, end] = getSourceposRows(listItem.dataset.sourcepos);
    for (let i = start; i <= end; i += 1) {
      newSourcePositions.push(i);
    }
  });

  return newSourcePositions;
};

/**
 * Converts a description to one with a new list sort order.
 *
 * Given a description like:
 *
 * <pre>
 * 1. I am text
 * 2.
 * 3. - Item 1
 * 4. - Item 2
 * 5.   - Item 3
 * 6.   - Item 4
 * 7. - Item 5
 * </pre>
 *
 * And a reordered list (due to dragging Item 2 into Item 1's position) like:
 *
 * <pre>
 * <ul data-sourcepos="3:1-7:8">
 *   <li data-sourcepos="4:1-6:10">
 *     Item 2
 *     <ul data-sourcepos="5:3-6:10">
 *       <li data-sourcepos="5:3-5:10">Item 3</li>
 *       <li data-sourcepos="6:3-6:10">Item 4</li>
 *     </ul>
 *   </li>
 *   <li data-sourcepos="3:1-3:8">Item 1</li>
 *   <li data-sourcepos="7:1-7:8">Item 5</li>
 * </ul>
 * </pre>
 *
 * This function returns:
 *
 * <pre>
 * 1. I am text
 * 2.
 * 3. - Item 2
 * 4.   - Item 3
 * 5.   - Item 4
 * 6. - Item 1
 * 7. - Item 5
 * </pre>
 *
 * @param {String} description Description in markdown format
 * @param {HTMLElement} list A `ul` or `ol` element containing a new sort order
 * @returns {String} Markdown with a new list sort order
 */
export const convertDescriptionWithNewSort = (description, list) => {
  const descriptionLines = description.split(NEWLINE);
  const [startIndexOfList] = getSourceposRows(list.dataset.sourcepos);

  getNewSourcePositions(list)
    .map((lineIndex) => descriptionLines[lineIndex])
    .forEach((line, index) => {
      descriptionLines[startIndexOfList + index] = line;
    });

  return descriptionLines.join(NEWLINE);
};

const bulletTaskListItemRegex = /^\s*[-*]\s+\[.]\s+/;
const numericalTaskListItemRegex = /^\s*[0-9]\.\s+\[.]\s+/;
const codeMarkdownRegex = /^\s*`.*`\s*$/;
const imageOrLinkMarkdownRegex = /^\s*!?\[.*\)\s*$/;

/**
 * Checks whether the line of markdown contains a task list item,
 * i.e. `- [ ]`, `* [ ]`, or `1. [ ]`.
 *
 * @param {String} line A line of markdown
 * @returns {boolean} `true` if the line contains a task list item, otherwise `false`
 */
const containsTaskListItem = (line) =>
  bulletTaskListItemRegex.test(line) || numericalTaskListItemRegex.test(line);

/**
 * Deletes a task list item from the description.
 *
 * Starting from the task list item, it deletes each line until it hits a nested
 * task list item and reduces the indentation of each line from this line onwards.
 *
 * For example, for a given description like:
 *
 * <pre>
 * 1. [ ] item 1
 *
 *    paragraph text
 *
 *    1. [ ] item 2
 *
 *       paragraph text
 *
 *    1. [ ] item 3
 * </pre>
 *
 * Then when prompted to delete item 1, this function will return:
 *
 * <pre>
 * 1. [ ] item 2
 *
 *    paragraph text
 *
 * 1. [ ] item 3
 * </pre>
 *
 * @param {String} description Description in markdown format
 * @param {String} sourcepos Source position in format `23:3-23:14`
 * @returns {{newDescription: String, taskDescription: String, taskTitle: String}} Object with:
 *
 * - `newDescription` property that contains markdown with the deleted task list item omitted
 * - `taskDescription` property that contains the description of the deleted task list item
 * - `taskTitle` property that contains the title of the deleted task list item
 */
export const deleteTaskListItem = (description, sourcepos) => {
  const descriptionLines = description.split(NEWLINE);
  const [startIndex, endIndex] = getSourceposRows(sourcepos);

  const firstLine = descriptionLines[startIndex];
  const firstLineIndentation = firstLine.length - firstLine.trimStart().length;

  const taskTitle = firstLine
    .replace(bulletTaskListItemRegex, '')
    .replace(numericalTaskListItemRegex, '');
  const taskDescription = [];

  let indentation = 0;
  let linesToDelete = 1;
  let reduceIndentation = false;

  for (let i = startIndex + 1; i <= endIndex; i += 1) {
    if (reduceIndentation) {
      descriptionLines[i] = descriptionLines[i].slice(indentation);
    } else if (containsTaskListItem(descriptionLines[i])) {
      reduceIndentation = true;
      const currentLine = descriptionLines[i];
      const currentLineIndentation = currentLine.length - currentLine.trimStart().length;
      indentation = currentLineIndentation - firstLineIndentation;
      descriptionLines[i] = descriptionLines[i].slice(indentation);
    } else {
      taskDescription.push(descriptionLines[i].trimStart());
      linesToDelete += 1;
    }
  }

  descriptionLines.splice(startIndex, linesToDelete);

  return {
    newDescription: descriptionLines.join(NEWLINE),
    taskDescription: taskDescription.join(NEWLINE) || undefined,
    taskTitle,
  };
};

/**
 * Given a title and description for a task:
 *
 * - Moves characters beyond the 255 character limit from the title to the description
 * - Moves a pure markdown title to the description and gives the title the value `Untitled`
 *
 * @param {String} taskTitle The task title
 * @param {String} taskDescription The task description
 * @returns {{description: String, title: String}} An object with the formatted task title and description
 */
export const extractTaskTitleAndDescription = (taskTitle, taskDescription) => {
  const isTitleOnlyMarkdown =
    codeMarkdownRegex.test(taskTitle) || imageOrLinkMarkdownRegex.test(taskTitle);

  if (isTitleOnlyMarkdown) {
    return {
      title: __('Untitled'),
      description: taskDescription
        ? taskTitle.concat(NEWLINE, NEWLINE, taskDescription)
        : taskTitle,
    };
  }

  const isTitleTooLong = taskTitle.length > TITLE_LENGTH_MAX;

  if (isTitleTooLong) {
    return {
      title: taskTitle.slice(0, TITLE_LENGTH_MAX),
      description: taskDescription
        ? taskTitle.slice(TITLE_LENGTH_MAX).concat(NEWLINE, NEWLINE, taskDescription)
        : taskTitle.slice(TITLE_LENGTH_MAX),
    };
  }

  return {
    title: taskTitle,
    description: taskDescription,
  };
};