/**
* 问卷内容(`…` 行)的**解析**——纯函数,可被 harness 直接测。
*
* 为什么单独抽出来(2026-09-24):渲染层交给 `QuestionCard` 的 `content` 是
* **带标记的整段**(切行器 `StreamXMLFilter` 给 block 行产出的就是 `openTag + inner + closeTag`,
* 见 `handleBehavior`)。而组件的两个解析函数一个剥标记、一个不剥 ——
* 于是"作答后写回 `{answers:…}`"时只读分支解析失败、交互分支也没有 `questions`,
* **整张卡片渲染成空**(用户报「提交完问卷直接看不见了」)。
* 现在两处都走这里的同一个解包逻辑,并且 harness 会用**真实切行器**跑一遍往返。
*/
export interface QuestionOption {
label: string;
description?: string;
recommended?: boolean;
[key: string]: unknown;
}
export interface Question {
header: string;
question: string;
multiSelect?: boolean;
allowFreeformInput?: boolean;
message?: string;
/** 输入框常驻并排在最上(用于补充信息),默认 false 保持原有"其他"行为 */
freeformInputOnTop?: boolean;
/** 上述常驻输入框的 placeholder */
freeformPlaceholder?: string;
options?: QuestionOption[];
}
/** 问卷答案(`QuestionCard` 提交时给出的形状,也是只读渲染读的形状) */
export interface QuestionCardAnswer {
header?: string;
selected?: string[];
freeText?: string;
skipped?: boolean;
}
const MARKER_RE = /]*>([\s\S]*?)<\/question-cards>/i;
/**
* 取出标记内部的 JSON 文本。
*
* - 内容带 `` 标记 → 取标记内部(两处解析都靠它)
* - 不带标记 → 原样返回(兼容直接传 JSON 的调用方)
*/
export const unwrapQuestionCardsContent = (content: string): string => {
const text = String(content ?? "").trim();
const match = text.match(MARKER_RE);
return (match ? match[1] : text).trim();
};
const safeParse = (text: string): any => {
try {
return JSON.parse(text);
} catch {
return null;
}
};
/** 解析问卷题目(交互态渲染用);解析不出来返回空数组 */
export const parseQuestionsFromContent = (content: string): Question[] => {
const parsed = safeParse(unwrapQuestionCardsContent(content));
const questions = parsed?.questions;
return Array.isArray(questions) ? (questions as Question[]) : [];
};
/** 解析已作答结果(只读态渲染用);没有答案返回 null */
export const parseAnswersFromContent = (content: string): QuestionCardAnswer[] | null => {
const parsed = safeParse(unwrapQuestionCardsContent(content));
const answers = parsed?.answers;
return Array.isArray(answers) ? (answers as QuestionCardAnswer[]) : null;
};