@antv/narrative-text-schema
Version:
Json schema of narrative text visualization
128 lines (116 loc) • 4.96 kB
text/typescript
import { isUndefined, arr2map } from '../utils';
import type { NarrativeTextSpec, ParagraphSpec, PhraseSpec, TextParagraphSpec, TextPhraseSpec } from '../schema';
import type { Structure, Variable, StructureTemp } from './types';
/**
* use structure and variables to generate narrative text spec
*/
function generateTextSpec({
structures,
structureTemps = [],
variables = [],
}: {
structures: Structure[];
structureTemps?: StructureTemp[];
variables?: Variable[];
}): NarrativeTextSpec {
const variableMap = arr2map<Variable>(variables, 'variableId');
const structureTempMap = arr2map<StructureTemp>(structureTemps, 'templateId');
const splitReg = /(&{.*?})/;
const varReg = /&{(.*?)}/;
// generate paragraphs by structure
let paragraphs: ParagraphSpec[] = [];
structures.forEach((structure, structureIdx) => {
const { template } = structure;
let { displayType = structureIdx === 0 ? 'paragraph' : 'phrase' } = structure;
// "System &{s1}, includes &{s2}." => ["System ", "&{s1}", ", includes ", "&{s2}", "."]
const templateStrArr = template.split(splitReg).filter((str) => str);
templateStrArr.forEach((templateStr, structureTempIdx) => {
const templateId: string | undefined = varReg.exec(templateStr)?.[1];
if (structureTempIdx > 0) displayType = 'phrase';
if (templateId && structureTempMap[templateId]) {
const structureTemp = structureTempMap[templateId];
const { template: subTemplate, variableId, separator, displayType: subDisplayType = 'phrase' } = structureTemp;
const variable: Variable | undefined = variableId && variableMap[variableId];
const sentences = getSentences(subTemplate, variable?.dataValue, variable?.dataMetaMap);
if (subDisplayType === 'paragraph') {
paragraphs = [
...paragraphs,
...sentences.map<TextParagraphSpec>((sentence, sentenceIdx) => {
const phrases = [...sentence];
if (separator && sentenceIdx < sentences.length - 1) {
phrases.push({ type: 'text', value: separator });
}
return {
type: 'normal',
phrases,
};
}),
];
} else if (subDisplayType === 'phrase') {
if (!paragraphs.length) paragraphs.push({ type: 'normal', phrases: [] });
const latestParagraph = paragraphs[paragraphs.length - 1] as TextParagraphSpec;
latestParagraph.phrases = [
...latestParagraph.phrases,
...sentences.reduce((prev, curr, sentenceIdx) => {
const phrases = [...prev, ...curr];
if (separator && sentenceIdx < sentences.length - 1) {
phrases.push({ type: 'text', value: separator });
}
return phrases;
}, []),
];
}
} else {
const currPhrase: TextPhraseSpec = { type: 'text', value: templateStr };
if (displayType === 'paragraph') {
paragraphs.push({ type: 'normal', phrases: [currPhrase] });
} else if (displayType === 'phrase') {
if (!paragraphs.length) paragraphs.push({ type: 'normal', phrases: [] });
const latestParagraph = paragraphs[paragraphs.length - 1] as TextParagraphSpec;
latestParagraph.phrases.push(currPhrase);
}
}
});
});
return {
// single section represent
// 当前构造可以只通过一个 section 表示
sections: [{ paragraphs }],
};
}
function getSentences(
template: string,
dataValue?: Variable['dataValue'],
dataMetaMap?: Variable['dataMetaMap'],
): PhraseSpec[][] {
const splitReg = /(\${.*?})/;
const varReg = /\${(.*?)}/;
const METRIC_NAME_SUFFIX = '[metric_name]';
// "user ${name}, age is ${age}" => ["user ", "${name}", ", age is ", "${age}"]
const templateStrArr = template.split(splitReg).filter((str) => str);
// {} => [{}]
const formattedDataValue = Array.isArray(dataValue) ? dataValue : [dataValue || {}];
return formattedDataValue.map((datum) => {
return templateStrArr.map<PhraseSpec>((str) => {
const varName: string | undefined = varReg.exec(str)?.[1]; // age
if (varName) {
if (varName.endsWith(METRIC_NAME_SUFFIX)) {
const fieldId = varName.slice(0, -METRIC_NAME_SUFFIX.length);
return {
type: 'entity',
value: dataMetaMap?.[fieldId]?.name || fieldId,
metadata: { entityType: 'metric_name' },
};
}
const data = isUndefined(datum[varName]) ? '-' : `${datum[varName]}`;
const dataMeta = dataMetaMap?.[varName];
if (dataMeta?.entityType) {
return { type: 'entity', value: data, metadata: { entityType: dataMeta?.entityType } };
}
return { type: 'text', value: data };
}
return { type: 'text', value: str };
});
});
}
export default generateTextSpec;