Add internal mix tag input for calculator and goal tree formulas
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
This commit is contained in:
parent
5c8148c905
commit
ae503c7260
13 changed files with 929 additions and 23 deletions
123
website/src/utils/mixTagsInputUtils.js
Normal file
123
website/src/utils/mixTagsInputUtils.js
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
// Rôle : fournit les helpers purs pour les champs mix-tags à valeur string.
|
||||
const TOKEN_WORD_CHAR_PATTERN = /[\p{L}\p{N}_]/u;
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value ?? "");
|
||||
}
|
||||
|
||||
export function normalizeMixTagSpacing(value) {
|
||||
return normalizeText(value).replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
export function normalizeMixTagDefinitions(tokens = []) {
|
||||
const seen = new Set();
|
||||
return tokens
|
||||
.map((token) => ({
|
||||
...token,
|
||||
value: normalizeText(token.value),
|
||||
label: normalizeText(token.label || token.value)
|
||||
}))
|
||||
.filter((token) => {
|
||||
if (!token.value || seen.has(token.value)) return false;
|
||||
seen.add(token.value);
|
||||
return true;
|
||||
})
|
||||
.sort((left, right) => right.value.length - left.value.length);
|
||||
}
|
||||
|
||||
export function isIsolatedMixTagValue(text, start, length) {
|
||||
const before = start > 0 ? text[start - 1] : "";
|
||||
const after = start + length < text.length ? text[start + length] : "";
|
||||
return (!before || !TOKEN_WORD_CHAR_PATTERN.test(before)) && (!after || !TOKEN_WORD_CHAR_PATTERN.test(after));
|
||||
}
|
||||
|
||||
export function getMixTagRanges(value, tokens = []) {
|
||||
const text = normalizeText(value);
|
||||
const definitions = normalizeMixTagDefinitions(tokens);
|
||||
const ranges = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < text.length) {
|
||||
const token = definitions.find((candidate) => (
|
||||
text.slice(index, index + candidate.value.length).toLowerCase() === candidate.value.toLowerCase() && isIsolatedMixTagValue(text, index, candidate.value.length)
|
||||
));
|
||||
if (token) {
|
||||
ranges.push({ start: index, end: index + token.value.length, token });
|
||||
index += token.value.length;
|
||||
continue;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
export function normalizeMixTagText(value, tokens = []) {
|
||||
const text = normalizeMixTagSpacing(value);
|
||||
const ranges = getMixTagRanges(text, tokens);
|
||||
if (!ranges.length) return text;
|
||||
let cursor = 0;
|
||||
let normalized = "";
|
||||
|
||||
ranges.forEach((range) => {
|
||||
normalized += text.slice(cursor, range.start);
|
||||
normalized += range.token.value;
|
||||
cursor = range.end;
|
||||
});
|
||||
|
||||
return `${normalized}${text.slice(cursor)}`;
|
||||
}
|
||||
|
||||
export function tokenizeMixTagValue(value, tokens = []) {
|
||||
const text = normalizeText(value);
|
||||
const ranges = getMixTagRanges(text, tokens);
|
||||
const segments = [];
|
||||
let cursor = 0;
|
||||
|
||||
ranges.forEach((range) => {
|
||||
if (range.start > cursor) segments.push({ type: "text", text: text.slice(cursor, range.start), start: cursor, end: range.start });
|
||||
segments.push({ type: "token", token: range.token, text: text.slice(range.start, range.end), start: range.start, end: range.end });
|
||||
cursor = range.end;
|
||||
});
|
||||
|
||||
if (cursor < text.length) segments.push({ type: "text", text: text.slice(cursor), start: cursor, end: text.length });
|
||||
return segments;
|
||||
}
|
||||
|
||||
export function buildInsertedMixTagValue(value, start, end, tokenValue, { formulaSpacing = false } = {}) {
|
||||
const text = normalizeText(value);
|
||||
const selectionStart = Math.max(0, Math.min(start, end));
|
||||
const selectionEnd = Math.max(selectionStart, Math.max(start, end));
|
||||
const before = text.slice(0, selectionStart);
|
||||
const after = text.slice(selectionEnd);
|
||||
const normalizedTokenValue = normalizeText(tokenValue);
|
||||
const needsSpaceBefore = formulaSpacing && before && !/[\s(+\-*/]$/.test(before);
|
||||
const needsSpaceAfter = formulaSpacing && after && !/^[\s)+\-*/]/.test(after);
|
||||
const inserted = `${needsSpaceBefore ? " " : ""}${normalizedTokenValue}${needsSpaceAfter ? " " : ""}`;
|
||||
|
||||
return {
|
||||
value: `${before}${inserted}${after}`,
|
||||
cursor: before.length + inserted.length
|
||||
};
|
||||
}
|
||||
|
||||
export function removeMixTagAtOffset(value, start, end, tokens = [], direction = "backward") {
|
||||
const text = normalizeText(value);
|
||||
const selectionStart = Math.max(0, Math.min(start, end));
|
||||
const selectionEnd = Math.max(selectionStart, Math.max(start, end));
|
||||
const token = getMixTagRanges(text, tokens).find((range) => {
|
||||
if (selectionStart !== selectionEnd) return selectionStart < range.end && selectionEnd > range.start;
|
||||
return direction === "backward"
|
||||
? selectionStart > range.start && selectionStart <= range.end
|
||||
: selectionStart >= range.start && selectionStart < range.end;
|
||||
});
|
||||
|
||||
if (!token) return null;
|
||||
const removeStart = selectionStart === selectionEnd ? token.start : Math.min(selectionStart, token.start);
|
||||
const removeEnd = selectionStart === selectionEnd ? token.end : Math.max(selectionEnd, token.end);
|
||||
|
||||
return {
|
||||
value: `${text.slice(0, removeStart)}${text.slice(removeEnd)}`,
|
||||
cursor: removeStart
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue