@jskit-ai/assistant-core 0.1.164 → 0.1.166
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/App.vue +104 -0
- package/examples/conversation/README.md +42 -0
- package/examples/conversation/dist/assets/index-CSTXm6q3.css +1 -0
- package/examples/conversation/dist/assets/index-DGgOLzoo.js +30 -0
- package/examples/conversation/dist/index.html +4 -0
- package/examples/conversation/index.html +2 -0
- package/examples/conversation/main.js +9 -0
- package/examples/conversation/node_modules/.vite/deps/@mdi_js.js +7450 -0
- package/examples/conversation/node_modules/.vite/deps/@mdi_js.js.map +1 -0
- package/examples/conversation/node_modules/.vite/deps/VTooltip-jIxO6ULs.js +2311 -0
- package/examples/conversation/node_modules/.vite/deps/VTooltip-jIxO6ULs.js.map +1 -0
- package/examples/conversation/node_modules/.vite/deps/_metadata.json +70 -0
- package/examples/conversation/node_modules/.vite/deps/defineComponent-DK6LSjDW.js +710 -0
- package/examples/conversation/node_modules/.vite/deps/defineComponent-DK6LSjDW.js.map +1 -0
- package/examples/conversation/node_modules/.vite/deps/hotkey-parsing-DnoZxtbW.js +1214 -0
- package/examples/conversation/node_modules/.vite/deps/hotkey-parsing-DnoZxtbW.js.map +1 -0
- package/examples/conversation/node_modules/.vite/deps/icons-ze2hmg7j.js +98 -0
- package/examples/conversation/node_modules/.vite/deps/icons-ze2hmg7j.js.map +1 -0
- package/examples/conversation/node_modules/.vite/deps/package.json +3 -0
- package/examples/conversation/node_modules/.vite/deps/theme-D78LVcJg.js +1301 -0
- package/examples/conversation/node_modules/.vite/deps/theme-D78LVcJg.js.map +1 -0
- package/examples/conversation/node_modules/.vite/deps/vue.js +2 -0
- package/examples/conversation/node_modules/.vite/deps/vue.runtime.esm-bundler-YcakWsDn.js +8538 -0
- package/examples/conversation/node_modules/.vite/deps/vue.runtime.esm-bundler-YcakWsDn.js.map +1 -0
- package/examples/conversation/node_modules/.vite/deps/vuetify.js +413 -0
- package/examples/conversation/node_modules/.vite/deps/vuetify.js.map +1 -0
- package/examples/conversation/node_modules/.vite/deps/vuetify_components.js +25370 -0
- package/examples/conversation/node_modules/.vite/deps/vuetify_components.js.map +1 -0
- package/examples/conversation/node_modules/.vite/deps/vuetify_directives.js +132 -0
- package/examples/conversation/node_modules/.vite/deps/vuetify_directives.js.map +1 -0
- package/examples/conversation/node_modules/.vite/deps/vuetify_iconsets_mdi-svg.js +70 -0
- package/examples/conversation/node_modules/.vite/deps/vuetify_iconsets_mdi-svg.js.map +1 -0
- package/examples/conversation/package.json +22 -0
- package/examples/conversation/server.js +123 -0
- package/examples/conversation/vite.config.js +8 -0
- package/package.json +59 -7
- package/src/client/conversation/AssistantComposerActions.vue +43 -0
- package/src/client/conversation/AssistantConversationElement.vue +78 -0
- package/src/client/conversation/AssistantProgress.vue +88 -0
- package/src/client/conversation/AssistantPromptInput.vue +467 -0
- package/src/client/conversation/AssistantTranscript.vue +1361 -0
- package/src/client/conversation/LongTextInlineParts.vue +64 -0
- package/src/client/conversation/LongTextPreviewBlocks.vue +324 -0
- package/src/client/conversation/index.js +9 -0
- package/src/client/conversation/submitText.js +29 -0
- package/src/client/conversation/useScrollToBottom.js +151 -0
- package/src/server/conversation/codexClient.js +328 -0
- package/src/server/conversation/codexEvents.js +623 -0
- package/src/server/conversation/codexTurn.js +280 -0
- package/src/server/conversation/index.js +2 -0
- package/src/server/conversation/memoryStorage.js +76 -0
- package/src/server/conversation/normalize.js +7 -0
- package/src/server/conversation/openCodeClient.js +512 -0
- package/src/server/conversation/transcript.js +92 -0
- package/src/shared/conversation/answerChoiceSugar.js +191 -0
- package/src/shared/conversation/index.js +6 -0
- package/src/shared/conversation/longTextBlocks.js +403 -0
- package/src/shared/conversation/numberedQuestionSugar.js +295 -0
- package/src/shared/conversation/scrollFollowState.js +32 -0
- package/src/shared/conversation/thinkingText.js +13 -0
- package/src/shared/conversation/turns.js +30 -0
- package/src/testing/conversationStorageContract.js +35 -0
- package/test/conversation.test.js +57 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const UI_ANSWER_CHOICE_FIELD = "__ui_answer_choice";
|
|
2
|
+
const ANSWER_CHOICE_MIN_COUNT = 2;
|
|
3
|
+
const ANSWER_CHOICE_MAX_COUNT = 6;
|
|
4
|
+
const ANSWER_CHOICE_LABEL_MAX_LENGTH = 96;
|
|
5
|
+
const ANSWER_CHOICE_VALUE_MAX_LENGTH = 320;
|
|
6
|
+
|
|
7
|
+
function inactiveAnswerChoiceSugar() {
|
|
8
|
+
return {
|
|
9
|
+
choices: []
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizedChoiceText(value = "") {
|
|
14
|
+
return String(value || "").trim().replace(/\s+/gu, " ");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isSingleTextareaMessageField(fields = [], fieldName = "response") {
|
|
18
|
+
return fields.length === 1 &&
|
|
19
|
+
fields[0]?.name === fieldName &&
|
|
20
|
+
fields[0]?.kind === "textarea";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function optionalValueMatches(actual = "", expected = "") {
|
|
24
|
+
return !expected || String(actual || "") === String(expected || "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function canRenderAnswerChoiceSugar({
|
|
28
|
+
fields = [],
|
|
29
|
+
fieldName = "response",
|
|
30
|
+
intentId = "",
|
|
31
|
+
requiredIntentId = "",
|
|
32
|
+
requiredStepStatus = "",
|
|
33
|
+
stepStatus = ""
|
|
34
|
+
} = {}) {
|
|
35
|
+
return Boolean(
|
|
36
|
+
isSingleTextareaMessageField(fields, fieldName) &&
|
|
37
|
+
optionalValueMatches(intentId, requiredIntentId) &&
|
|
38
|
+
optionalValueMatches(stepStatus, requiredStepStatus)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function answerChoiceHeadingLine(line = "") {
|
|
43
|
+
return /^(possible answers|choices):$/iu.test(String(line || "").trim());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function answerChoiceLineText(line = "") {
|
|
47
|
+
const match = String(line || "").trim().match(/^[-*]\s+(.+)$/u);
|
|
48
|
+
return match ? normalizedChoiceText(match[1]) : "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function answerChoiceFromLine(line = "") {
|
|
52
|
+
const text = answerChoiceLineText(line);
|
|
53
|
+
if (!text || text.length > ANSWER_CHOICE_VALUE_MAX_LENGTH) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const recommendedMatch = text.match(/^(.{1,96}?)\s+\(recommended\)$/iu);
|
|
58
|
+
if (recommendedMatch) {
|
|
59
|
+
const label = normalizedChoiceText(recommendedMatch[1]);
|
|
60
|
+
return label ? { label, recommended: true, value: label } : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const colonMatch = text.match(/^([^:]{1,96}):\s+(.+)$/u);
|
|
64
|
+
if (colonMatch) {
|
|
65
|
+
const label = normalizedChoiceText(colonMatch[1]);
|
|
66
|
+
const value = normalizedChoiceText(colonMatch[2]);
|
|
67
|
+
return label && value ? { label, value } : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const parentheticalMatch = text.match(/^(.{1,96}?)\s+\(([^()]+)\)$/u);
|
|
71
|
+
if (parentheticalMatch) {
|
|
72
|
+
const rawLabel = normalizedChoiceText(parentheticalMatch[1]);
|
|
73
|
+
const markdownTokenMatch = rawLabel.match(/^(?:`([^`]+)`|\*\*([^*]+)\*\*)$/u);
|
|
74
|
+
const label = normalizedChoiceText(
|
|
75
|
+
markdownTokenMatch?.[1] || markdownTokenMatch?.[2] || rawLabel
|
|
76
|
+
);
|
|
77
|
+
if (markdownTokenMatch) {
|
|
78
|
+
return label ? { label, value: label } : null;
|
|
79
|
+
}
|
|
80
|
+
const value = normalizedChoiceText(parentheticalMatch[2]);
|
|
81
|
+
return label && value ? { label, value } : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
label: text,
|
|
86
|
+
value: text
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function validAnswerChoice(choice = {}) {
|
|
91
|
+
return Boolean(
|
|
92
|
+
choice &&
|
|
93
|
+
choice.label &&
|
|
94
|
+
choice.value &&
|
|
95
|
+
choice.label.length <= ANSWER_CHOICE_LABEL_MAX_LENGTH &&
|
|
96
|
+
choice.value.length <= ANSWER_CHOICE_VALUE_MAX_LENGTH
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseAnswerChoicePrompt(value = "") {
|
|
101
|
+
const lines = String(value || "").replace(/\r\n/gu, "\n").split("\n");
|
|
102
|
+
if (lines.some((line) => {
|
|
103
|
+
const match = String(line || "").trim().match(/^\[(?:Q)?\d+\]\s+(.+)/iu);
|
|
104
|
+
return match && /\?(?:[*_`”"'’)\]]*)$/u.test(match[1].trim());
|
|
105
|
+
})) {
|
|
106
|
+
return inactiveAnswerChoiceSugar();
|
|
107
|
+
}
|
|
108
|
+
const headingIndex = lines.findIndex(answerChoiceHeadingLine);
|
|
109
|
+
if (headingIndex < 0) {
|
|
110
|
+
return inactiveAnswerChoiceSugar();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const choices = [];
|
|
114
|
+
for (let index = headingIndex + 1; index < lines.length; index += 1) {
|
|
115
|
+
const line = lines[index];
|
|
116
|
+
if (!String(line || "").trim()) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const choice = answerChoiceFromLine(line);
|
|
120
|
+
if (!choice) {
|
|
121
|
+
if (choices.length) {
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
return inactiveAnswerChoiceSugar();
|
|
125
|
+
}
|
|
126
|
+
choices.push(choice);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const uniqueValues = new Set(choices.map((choice) => choice.value));
|
|
130
|
+
if (
|
|
131
|
+
choices.length < ANSWER_CHOICE_MIN_COUNT ||
|
|
132
|
+
choices.length > ANSWER_CHOICE_MAX_COUNT ||
|
|
133
|
+
uniqueValues.size !== choices.length ||
|
|
134
|
+
choices.some((choice) => !validAnswerChoice(choice))
|
|
135
|
+
) {
|
|
136
|
+
return inactiveAnswerChoiceSugar();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
choices
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function answerChoiceSugarForMessageInput({
|
|
145
|
+
fields = [],
|
|
146
|
+
fieldName = "response",
|
|
147
|
+
intentId = "",
|
|
148
|
+
message = "",
|
|
149
|
+
requiredIntentId = "",
|
|
150
|
+
requiredStepStatus = "",
|
|
151
|
+
stepStatus = ""
|
|
152
|
+
} = {}) {
|
|
153
|
+
if (!canRenderAnswerChoiceSugar({
|
|
154
|
+
fields,
|
|
155
|
+
fieldName,
|
|
156
|
+
intentId,
|
|
157
|
+
requiredIntentId,
|
|
158
|
+
requiredStepStatus,
|
|
159
|
+
stepStatus
|
|
160
|
+
})) {
|
|
161
|
+
return inactiveAnswerChoiceSugar();
|
|
162
|
+
}
|
|
163
|
+
return parseAnswerChoicePrompt(message);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function answerChoiceInputFields(choices = []) {
|
|
167
|
+
return [
|
|
168
|
+
{
|
|
169
|
+
choices,
|
|
170
|
+
kind: "answer_choices",
|
|
171
|
+
name: UI_ANSWER_CHOICE_FIELD,
|
|
172
|
+
required: false
|
|
173
|
+
}
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function answerChoiceSubmissionFields(choiceValue = "", fieldName = "response") {
|
|
178
|
+
return {
|
|
179
|
+
[fieldName]: normalizedChoiceText(choiceValue)
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export {
|
|
184
|
+
UI_ANSWER_CHOICE_FIELD,
|
|
185
|
+
answerChoiceFromLine,
|
|
186
|
+
answerChoiceInputFields,
|
|
187
|
+
answerChoiceSubmissionFields,
|
|
188
|
+
answerChoiceSugarForMessageInput,
|
|
189
|
+
canRenderAnswerChoiceSugar,
|
|
190
|
+
parseAnswerChoicePrompt
|
|
191
|
+
};
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
function parseLongTextReviewBlocks(value, options = {}) {
|
|
2
|
+
const lines = expandCompactPipeTableLines(String(value || "").replace(/\r\n/gu, "\n").split("\n"));
|
|
3
|
+
const blocks = [];
|
|
4
|
+
let paragraphLines = [];
|
|
5
|
+
let listBlock = null;
|
|
6
|
+
let codeLines = null;
|
|
7
|
+
let detailsBlock = null;
|
|
8
|
+
const preserveParagraphLineBreaks = options.preserveParagraphLineBreaks === true;
|
|
9
|
+
|
|
10
|
+
const parseDetailsOpening = (trimmed = "") => {
|
|
11
|
+
if (trimmed === "<details>") {
|
|
12
|
+
return {
|
|
13
|
+
closed: false,
|
|
14
|
+
remainder: "",
|
|
15
|
+
summary: "Details"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const inlineMatch = trimmed.match(/^<details>\s*<summary>\s*(.*?)\s*<\/summary>\s*(.*)$/u);
|
|
19
|
+
if (!inlineMatch) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
let remainder = inlineMatch[2] || "";
|
|
23
|
+
let closed = false;
|
|
24
|
+
const closingMatch = remainder.match(/^(.*?)\s*<\/details>$/u);
|
|
25
|
+
if (closingMatch) {
|
|
26
|
+
remainder = closingMatch[1] || "";
|
|
27
|
+
closed = true;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
closed,
|
|
31
|
+
remainder: remainder.trim(),
|
|
32
|
+
summary: inlineMatch[1].trim() || "Details"
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const flushParagraph = () => {
|
|
37
|
+
const text = preserveParagraphLineBreaks
|
|
38
|
+
? paragraphLines.join("\n").trim()
|
|
39
|
+
: paragraphLines.join(" ").replace(/\s+/gu, " ").trim();
|
|
40
|
+
paragraphLines = [];
|
|
41
|
+
if (text) {
|
|
42
|
+
blocks.push({
|
|
43
|
+
text,
|
|
44
|
+
type: "paragraph"
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const flushList = () => {
|
|
50
|
+
if (listBlock?.items.length) {
|
|
51
|
+
blocks.push(listBlock);
|
|
52
|
+
}
|
|
53
|
+
listBlock = null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const flushCode = () => {
|
|
57
|
+
if (codeLines) {
|
|
58
|
+
blocks.push({
|
|
59
|
+
text: codeLines.join("\n").replace(/\n+$/u, ""),
|
|
60
|
+
type: "code"
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
codeLines = null;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const flushDetails = () => {
|
|
67
|
+
if (detailsBlock) {
|
|
68
|
+
blocks.push({
|
|
69
|
+
blocks: parseLongTextReviewBlocks(detailsBlock.lines.join("\n"), options),
|
|
70
|
+
summary: detailsBlock.summary || "Details",
|
|
71
|
+
type: "details"
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
detailsBlock = null;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
78
|
+
const rawLine = lines[lineIndex];
|
|
79
|
+
const line = rawLine.replace(/\s+$/u, "");
|
|
80
|
+
const trimmed = line.trim();
|
|
81
|
+
|
|
82
|
+
if (detailsBlock) {
|
|
83
|
+
const summaryMatch = trimmed.match(/^<summary>\s*(.*?)\s*<\/summary>$/u);
|
|
84
|
+
const closingMatch = trimmed.match(/^(.*?)\s*<\/details>$/u);
|
|
85
|
+
if (/^<\/details>$/u.test(trimmed)) {
|
|
86
|
+
flushDetails();
|
|
87
|
+
} else if (closingMatch) {
|
|
88
|
+
const beforeClose = closingMatch[1].trim();
|
|
89
|
+
if (beforeClose) {
|
|
90
|
+
detailsBlock.lines.push(beforeClose);
|
|
91
|
+
}
|
|
92
|
+
flushDetails();
|
|
93
|
+
} else if (summaryMatch) {
|
|
94
|
+
detailsBlock.summary = summaryMatch[1].trim() || detailsBlock.summary;
|
|
95
|
+
} else {
|
|
96
|
+
detailsBlock.lines.push(line);
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (codeLines) {
|
|
102
|
+
if (/^```/u.test(trimmed)) {
|
|
103
|
+
flushCode();
|
|
104
|
+
} else {
|
|
105
|
+
codeLines.push(line);
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (/^```/u.test(trimmed)) {
|
|
111
|
+
flushParagraph();
|
|
112
|
+
flushList();
|
|
113
|
+
codeLines = [];
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const detailsOpening = parseDetailsOpening(trimmed);
|
|
118
|
+
if (detailsOpening) {
|
|
119
|
+
flushParagraph();
|
|
120
|
+
flushList();
|
|
121
|
+
detailsBlock = {
|
|
122
|
+
lines: detailsOpening.remainder ? [detailsOpening.remainder] : [],
|
|
123
|
+
summary: detailsOpening.summary
|
|
124
|
+
};
|
|
125
|
+
if (detailsOpening.closed) {
|
|
126
|
+
flushDetails();
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!trimmed) {
|
|
132
|
+
flushParagraph();
|
|
133
|
+
flushList();
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const tableBlock = parsePipeTableAt(lines, lineIndex);
|
|
138
|
+
if (tableBlock) {
|
|
139
|
+
flushParagraph();
|
|
140
|
+
flushList();
|
|
141
|
+
blocks.push(tableBlock.block);
|
|
142
|
+
lineIndex = tableBlock.endIndex;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/u);
|
|
147
|
+
if (headingMatch) {
|
|
148
|
+
flushParagraph();
|
|
149
|
+
flushList();
|
|
150
|
+
blocks.push({
|
|
151
|
+
level: headingMatch[1].length,
|
|
152
|
+
text: headingMatch[2].trim(),
|
|
153
|
+
type: "heading"
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const unorderedMatch = trimmed.match(/^[-*+•]\s+(.*)$/u);
|
|
159
|
+
const orderedMatch = trimmed.match(/^\d+[.)]\s+(.*)$/u);
|
|
160
|
+
if (unorderedMatch || orderedMatch) {
|
|
161
|
+
flushParagraph();
|
|
162
|
+
const type = orderedMatch ? "ol" : "ul";
|
|
163
|
+
if (!listBlock || listBlock.type !== type) {
|
|
164
|
+
flushList();
|
|
165
|
+
listBlock = {
|
|
166
|
+
items: [],
|
|
167
|
+
type
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
listBlock.items.push({
|
|
171
|
+
text: (orderedMatch?.[1] || unorderedMatch?.[1] || "").trim()
|
|
172
|
+
});
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (listBlock) {
|
|
177
|
+
listBlock.items.at(-1).text += ` ${trimmed}`;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
paragraphLines.push(trimmed);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
flushParagraph();
|
|
184
|
+
flushList();
|
|
185
|
+
flushCode();
|
|
186
|
+
flushDetails();
|
|
187
|
+
return blocks;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function expandCompactPipeTableLines(lines = []) {
|
|
191
|
+
return lines.flatMap((line) => {
|
|
192
|
+
const text = String(line || "");
|
|
193
|
+
const trimmed = text.trim();
|
|
194
|
+
const compactRowBoundaries = text.match(/\|\s+\|/gu) || [];
|
|
195
|
+
if (!trimmed.startsWith("|") || compactRowBoundaries.length < 2) {
|
|
196
|
+
return [line];
|
|
197
|
+
}
|
|
198
|
+
const expandedLines = text.replace(/\|\s+\|/gu, "|\n|").split("\n");
|
|
199
|
+
if (!tableAlignments(parsePipeTableRow(expandedLines[1]))) {
|
|
200
|
+
return [line];
|
|
201
|
+
}
|
|
202
|
+
return expandedLines;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parsePipeTableAt(lines = [], startIndex = 0) {
|
|
207
|
+
const headers = parsePipeTableRow(lines[startIndex]);
|
|
208
|
+
const alignments = tableAlignments(parsePipeTableRow(lines[startIndex + 1]));
|
|
209
|
+
if (!headers || !alignments || headers.length !== alignments.length) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
if (!headers.some((header) => header)) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const rows = [];
|
|
217
|
+
let rowIndex = startIndex + 2;
|
|
218
|
+
while (rowIndex < lines.length) {
|
|
219
|
+
const row = parsePipeTableRow(lines[rowIndex]);
|
|
220
|
+
if (!row || row.length !== headers.length) {
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
rows.push(row);
|
|
224
|
+
rowIndex += 1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
block: {
|
|
229
|
+
alignments,
|
|
230
|
+
headers,
|
|
231
|
+
rows,
|
|
232
|
+
type: "table"
|
|
233
|
+
},
|
|
234
|
+
endIndex: rowIndex - 1
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function parsePipeTableRow(value = "") {
|
|
239
|
+
const trimmed = String(value || "").trim();
|
|
240
|
+
if (!trimmed.startsWith("|") || !trimmed.includes("|", 1)) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const cells = trimmed
|
|
244
|
+
.replace(/^\|/u, "")
|
|
245
|
+
.replace(/\|$/u, "")
|
|
246
|
+
.split("|")
|
|
247
|
+
.map((cell) => cell.trim());
|
|
248
|
+
return cells.length >= 2 ? cells : null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function tableAlignments(cells = null) {
|
|
252
|
+
if (!Array.isArray(cells) || !cells.length) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const alignments = [];
|
|
256
|
+
for (const cell of cells) {
|
|
257
|
+
const marker = String(cell || "").replace(/\s+/gu, "");
|
|
258
|
+
if (!/^:?-{3,}:?$/u.test(marker)) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (marker.startsWith(":") && marker.endsWith(":")) {
|
|
262
|
+
alignments.push("center");
|
|
263
|
+
} else if (marker.endsWith(":")) {
|
|
264
|
+
alignments.push("right");
|
|
265
|
+
} else {
|
|
266
|
+
alignments.push("left");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return alignments;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function pushInlineTextPart(parts = [], text = "") {
|
|
273
|
+
if (!text) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const previous = parts.at(-1);
|
|
277
|
+
if (previous?.type === "text") {
|
|
278
|
+
previous.text += text;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
parts.push({
|
|
282
|
+
text,
|
|
283
|
+
type: "text"
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function closingMarkdownBracketIndex(text = "", start = 0) {
|
|
288
|
+
let depth = 1;
|
|
289
|
+
for (let index = start + 1; index < text.length; index += 1) {
|
|
290
|
+
const character = text[index];
|
|
291
|
+
if (character === "\\") {
|
|
292
|
+
index += 1;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (character === "[") {
|
|
296
|
+
depth += 1;
|
|
297
|
+
} else if (character === "]") {
|
|
298
|
+
depth -= 1;
|
|
299
|
+
if (depth === 0) {
|
|
300
|
+
return index;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return -1;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function closingMarkdownDestinationIndex(text = "", start = 0) {
|
|
308
|
+
for (let index = start + 1; index < text.length; index += 1) {
|
|
309
|
+
if (text[index] === "\\") {
|
|
310
|
+
index += 1;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (text[index] === ")") {
|
|
314
|
+
return index;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return -1;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function normalizeMarkdownLinkHref(value = "") {
|
|
321
|
+
const href = String(value || "").trim().replace(/^<|>$/gu, "");
|
|
322
|
+
// eslint-disable-next-line no-control-regex -- Strip intentional ASCII controls before checking unsafe URL schemes.
|
|
323
|
+
const protocolText = href.replace(/[\u0000-\u0020\u007f]/gu, "");
|
|
324
|
+
return /^(?:javascript|vbscript|data):/iu.test(protocolText) ? "" : href;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function markdownLinkAt(text = "", start = 0) {
|
|
328
|
+
if (text[start] !== "[") {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
const labelEnd = closingMarkdownBracketIndex(text, start);
|
|
332
|
+
if (labelEnd === -1 || text[labelEnd + 1] !== "(") {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
const destinationStart = labelEnd + 1;
|
|
336
|
+
const destinationEnd = closingMarkdownDestinationIndex(text, destinationStart);
|
|
337
|
+
if (destinationEnd === -1) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const label = text.slice(start + 1, labelEnd);
|
|
341
|
+
const href = normalizeMarkdownLinkHref(text.slice(destinationStart + 1, destinationEnd));
|
|
342
|
+
if (!label || !href) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
end: destinationEnd + 1,
|
|
347
|
+
href,
|
|
348
|
+
text: label
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function parseLongTextInlineParts(value = "") {
|
|
353
|
+
const text = String(value || "");
|
|
354
|
+
const parts = [];
|
|
355
|
+
let cursor = 0;
|
|
356
|
+
while (cursor < text.length) {
|
|
357
|
+
if (text[cursor] === "[") {
|
|
358
|
+
const link = markdownLinkAt(text, cursor);
|
|
359
|
+
if (link) {
|
|
360
|
+
parts.push({
|
|
361
|
+
href: link.href,
|
|
362
|
+
text: link.text,
|
|
363
|
+
type: "link"
|
|
364
|
+
});
|
|
365
|
+
cursor = link.end;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (text[cursor] === "`") {
|
|
371
|
+
const end = text.indexOf("`", cursor + 1);
|
|
372
|
+
if (end > cursor + 1) {
|
|
373
|
+
parts.push({
|
|
374
|
+
text: text.slice(cursor + 1, end),
|
|
375
|
+
type: "code"
|
|
376
|
+
});
|
|
377
|
+
cursor = end + 1;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (text.startsWith("**", cursor)) {
|
|
383
|
+
const end = text.indexOf("**", cursor + 2);
|
|
384
|
+
if (end > cursor + 2) {
|
|
385
|
+
parts.push({
|
|
386
|
+
text: text.slice(cursor + 2, end),
|
|
387
|
+
type: "strong"
|
|
388
|
+
});
|
|
389
|
+
cursor = end + 2;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
pushInlineTextPart(parts, text[cursor]);
|
|
395
|
+
cursor += 1;
|
|
396
|
+
}
|
|
397
|
+
return parts.length ? parts : [{ text, type: "text" }];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export {
|
|
401
|
+
parseLongTextInlineParts,
|
|
402
|
+
parseLongTextReviewBlocks
|
|
403
|
+
};
|