debian-mirror-gitlab/app/assets/javascripts/issues/show/utils.js
2023-04-23 21:23:45 +05:30

230 lines
6.9 KiB
JavaScript

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,
};
};