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} 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} 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: * *
 * 1. I am text
 * 2.
 * 3. - Item 1
 * 4. - Item 2
 * 5.   - Item 3
 * 6.   - Item 4
 * 7. - Item 5
 * 
* * And a reordered list (due to dragging Item 2 into Item 1's position) like: * *
 * 
    *
  • * Item 2 *
      *
    • Item 3
    • *
    • Item 4
    • *
    *
  • *
  • Item 1
  • *
  • Item 5
  • *
*
* * This function returns: * *
 * 1. I am text
 * 2.
 * 3. - Item 2
 * 4.   - Item 3
 * 5.   - Item 4
 * 6. - Item 1
 * 7. - Item 5
 * 
* * @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: * *
 * 1. [ ] item 1
 *
 *    paragraph text
 *
 *    1. [ ] item 2
 *
 *       paragraph text
 *
 *    1. [ ] item 3
 * 
* * Then when prompted to delete item 1, this function will return: * *
 * 1. [ ] item 2
 *
 *    paragraph text
 *
 * 1. [ ] item 3
 * 
* * @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, }; };