147 lines
3.7 KiB
TypeScript
147 lines
3.7 KiB
TypeScript
import type {
|
|
AcceptanceCriteriaItemModel,
|
|
AcceptanceCriteriaModel,
|
|
} from '@/types/models/acceptance-criteria-model';
|
|
|
|
const MAX_INDENT_LEVEL = 8;
|
|
|
|
export type AcceptanceCriteriaEditorState = {
|
|
checklist: string;
|
|
summary: string;
|
|
};
|
|
|
|
export const emptyAcceptanceCriteria = (): AcceptanceCriteriaModel => ({
|
|
summary: null,
|
|
items: [],
|
|
});
|
|
|
|
export const normalizeAcceptanceCriteria = (
|
|
value: AcceptanceCriteriaModel | null | undefined,
|
|
): AcceptanceCriteriaModel => {
|
|
if (value === null || value === undefined) {
|
|
return emptyAcceptanceCriteria();
|
|
}
|
|
|
|
const summary = value.summary?.trim() ?? null;
|
|
const items = Array.isArray(value.items)
|
|
? value.items
|
|
.map((item) => ({
|
|
checked: item.checked === true,
|
|
indent: clampIndent(item.indent),
|
|
text: item.text.trim(),
|
|
}))
|
|
.filter((item) => item.text !== '')
|
|
: [];
|
|
|
|
return {
|
|
summary: summary === '' ? null : summary,
|
|
items,
|
|
};
|
|
};
|
|
|
|
export const parseChecklistLines = (
|
|
checklistInput: string,
|
|
): AcceptanceCriteriaItemModel[] => {
|
|
if (checklistInput.trim() === '') {
|
|
return [];
|
|
}
|
|
|
|
return checklistInput
|
|
.split(/\r?\n/u)
|
|
.map((line) => parseChecklistLine(line))
|
|
.filter(
|
|
(parsedLine): parsedLine is AcceptanceCriteriaItemModel =>
|
|
parsedLine !== null,
|
|
);
|
|
};
|
|
|
|
export const formatChecklistLines = (
|
|
items: AcceptanceCriteriaItemModel[],
|
|
): string => {
|
|
return items
|
|
.map((item) => {
|
|
const indentation = ' '.repeat(clampIndent(item.indent));
|
|
const marker = item.checked ? '[x]' : '[ ]';
|
|
|
|
return `${indentation}${marker} ${item.text}`;
|
|
})
|
|
.join('\n');
|
|
};
|
|
|
|
export const buildAcceptanceCriteria = (
|
|
summaryInput: string,
|
|
checklistInput: string,
|
|
): AcceptanceCriteriaModel => {
|
|
const summary = summaryInput.trim();
|
|
|
|
return normalizeAcceptanceCriteria({
|
|
summary: summary === '' ? null : summary,
|
|
items: parseChecklistLines(checklistInput),
|
|
});
|
|
};
|
|
|
|
export const acceptanceCriteriaToEditorState = (
|
|
value: AcceptanceCriteriaModel | null | undefined,
|
|
): AcceptanceCriteriaEditorState => {
|
|
const normalized = normalizeAcceptanceCriteria(value);
|
|
|
|
return {
|
|
checklist: formatChecklistLines(normalized.items),
|
|
summary: normalized.summary ?? '',
|
|
};
|
|
};
|
|
|
|
const parseChecklistLine = (
|
|
line: string,
|
|
): AcceptanceCriteriaItemModel | null => {
|
|
const normalizedLine = line.replaceAll('\t', ' ').trimEnd();
|
|
|
|
if (normalizedLine.trim() === '') {
|
|
return null;
|
|
}
|
|
|
|
const match = /^(?<indent>\s*)(?<content>.+)$/u.exec(normalizedLine);
|
|
|
|
if (match === null || match.groups === undefined) {
|
|
return null;
|
|
}
|
|
|
|
const indentLevel = clampIndent(
|
|
Math.floor(match.groups.indent.length / 2),
|
|
);
|
|
let content = match.groups.content.trim();
|
|
let checked = false;
|
|
|
|
const checkboxMatch =
|
|
/^(?:[-*+]\s+)?\[(?<state>[xX ])\]\s*(?<text>.+)$/u.exec(content);
|
|
|
|
if (checkboxMatch?.groups !== undefined) {
|
|
checked = checkboxMatch.groups.state.toLowerCase() === 'x';
|
|
content = checkboxMatch.groups.text.trim();
|
|
} else {
|
|
const bulletMatch = /^[-*+]\s+(?<text>.+)$/u.exec(content);
|
|
|
|
if (bulletMatch?.groups !== undefined) {
|
|
content = bulletMatch.groups.text.trim();
|
|
}
|
|
}
|
|
|
|
if (content === '') {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
checked,
|
|
indent: indentLevel,
|
|
text: content,
|
|
};
|
|
};
|
|
|
|
const clampIndent = (indent: number): number => {
|
|
if (!Number.isFinite(indent)) {
|
|
return 0;
|
|
}
|
|
|
|
return Math.max(0, Math.min(MAX_INDENT_LEVEL, Math.trunc(indent)));
|
|
};
|