Skip to content

Commit 875c29f

Browse files
tnagorrafrozenhelium
authored andcommitted
Refactor html parsing and template import logic
1 parent 983a461 commit 875c29f

File tree

3 files changed

+289
-199
lines changed

3 files changed

+289
-199
lines changed

app/src/utils/importTemplate.ts

Lines changed: 36 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -9,82 +9,10 @@ import {
99
} from '@togglecorp/fujs';
1010
import { type CellRichTextValue } from 'exceljs';
1111

12-
export function parseRichText(
13-
value: undefined,
14-
optionsMap?: TemplateFieldOptionsMapping,
15-
context?: { field: string, key: string }[],
16-
): undefined;
17-
export function parseRichText(
18-
value: string,
19-
optionsMap?: TemplateFieldOptionsMapping,
20-
context?: { field: string, key: string }[],
21-
): string | CellRichTextValue
22-
export function parseRichText(
23-
value: string | undefined,
24-
optionsMap?: TemplateFieldOptionsMapping,
25-
context?: { field: string, key: string }[],
26-
): string | CellRichTextValue | undefined
27-
export function parseRichText(
28-
value: string | undefined,
29-
optionsMap?: TemplateFieldOptionsMapping,
30-
context?: { field: string, key: string }[],
31-
): string | CellRichTextValue | undefined {
32-
if (isNotDefined(value)) {
33-
return value;
34-
}
35-
36-
const tagRegex = /(<\/?(?:b|u|i|ins)>)/;
37-
const tokens = value.split(tagRegex);
38-
39-
if (tokens.length === 1) {
40-
return value;
41-
}
42-
43-
const richText:CellRichTextValue['richText'] = [];
44-
45-
const stack: string[] = [];
46-
47-
const openTagRegex = /(<(?:b|u|i|ins)>)/;
48-
const closeTagRegex = /(<\/(?:b|u|i|ins)>)/;
49-
50-
tokens.forEach((token) => {
51-
if (token.match(openTagRegex)) {
52-
stack.push(token);
53-
return;
54-
}
55-
if (token.match(closeTagRegex)) {
56-
// TODO: Check correctness by checking closeTag with last openTag
57-
stack.pop();
58-
return;
59-
}
60-
if (stack.includes('<ins>')) {
61-
const [optionField, valueField] = token.split('.');
62-
const currOptions = context?.find((item) => item.field === optionField);
63-
const selectedOption = currOptions
64-
? optionsMap?.[optionField]?.find(
65-
(option) => String(option.key) === currOptions?.key,
66-
)
67-
: undefined;
68-
69-
richText.push({
70-
// FIXME: Need to add mechanism to identify if we have error for mapping
71-
text: selectedOption?.[valueField as 'description'] ?? '',
72-
});
73-
} else {
74-
richText.push({
75-
font: {
76-
bold: stack.includes('<b>'),
77-
italic: stack.includes('<i>'),
78-
underline: stack.includes('<u>'),
79-
},
80-
text: token,
81-
});
82-
}
83-
});
84-
// TODO: Check correctness to check that stack is empty
85-
86-
return { richText };
87-
}
12+
import {
13+
parsePseudoHtml,
14+
type ParsePlugin,
15+
} from '#utils/richText';
8816

8917
type ValidationType = string | number | boolean | 'textArea';
9018
type TypeToLiteral<T extends ValidationType> = T extends string
@@ -219,7 +147,30 @@ export function getCombinedKey(
219147

220148
export type TemplateField = HeadingTemplateField | InputTemplateField;
221149

222-
// TODO: add test
150+
function createInsPlugin(
151+
optionsMap: TemplateFieldOptionsMapping,
152+
context: { field: string, key: string }[],
153+
): ParsePlugin {
154+
return {
155+
tag: 'ins',
156+
transformer: (token, richText) => {
157+
const [optionField, valueField] = token.split('.');
158+
const currOptions = context?.find((item) => item.field === optionField);
159+
const selectedOption = currOptions
160+
? optionsMap?.[optionField]?.find(
161+
(option) => String(option.key) === currOptions?.key,
162+
)
163+
: undefined;
164+
165+
return {
166+
...richText,
167+
// FIXME: Need to add mechanism to identify if we have error for mapping
168+
text: selectedOption?.[valueField as 'description'] ?? '',
169+
};
170+
},
171+
};
172+
}
173+
223174
export function createImportTemplate<
224175
TEMPLATE_SCHEMA,
225176
OPTIONS_MAPPING extends TemplateFieldOptionsMapping
@@ -269,12 +220,14 @@ export function createImportTemplate<
269220
} satisfies HeadingTemplateField);
270221
}
271222

223+
const insPlugin = createInsPlugin(optionsMap, context);
224+
272225
if (schema.type === 'input') {
273226
const field = {
274227
type: 'input',
275228
name: fieldName,
276-
label: parseRichText(schema.label, optionsMap, context),
277-
description: parseRichText(schema.description, optionsMap, context),
229+
label: parsePseudoHtml(schema.label, [insPlugin]),
230+
description: parsePseudoHtml(schema.description, [insPlugin]),
278231
dataValidation: (schema.validation === 'number' || schema.validation === 'date' || schema.validation === 'integer' || schema.validation === 'textArea')
279232
? schema.validation
280233
: undefined,
@@ -290,8 +243,8 @@ export function createImportTemplate<
290243
const field = {
291244
type: 'input',
292245
name: fieldName,
293-
label: parseRichText(schema.label, optionsMap, context),
294-
description: parseRichText(schema.description, optionsMap, context),
246+
label: parsePseudoHtml(schema.label, [insPlugin]),
247+
description: parsePseudoHtml(schema.description, [insPlugin]),
295248
outlineLevel,
296249
dataValidation: 'list',
297250
optionsKey: schema.optionsKey,
@@ -322,8 +275,8 @@ export function createImportTemplate<
322275
context,
323276
} satisfies HeadingTemplateField;
324277

325-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
326278
const combinedKey = getCombinedKey(option.key, fieldName);
279+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
327280
const newFields = createImportTemplate<any, OPTIONS_MAPPING>(
328281
schema.children,
329282
optionsMap,
@@ -350,7 +303,6 @@ function addClientId(item: object): object {
350303
return { ...item, clientId: randomString() };
351304
}
352305

353-
// TODO: add test
354306
export function getValueFromImportTemplate<
355307
TEMPLATE_SCHEMA,
356308
OPTIONS_MAPPING extends TemplateFieldOptionsMapping,
@@ -437,6 +389,7 @@ export function getValueFromImportTemplate<
437389
return listValue;
438390
}
439391

392+
/*
440393
type TemplateName = 'dref-application' | 'dref-operational-update' | 'dref-final-report';
441394
442395
export interface ImportTemplateDescription<FormFields> {
@@ -448,7 +401,6 @@ export interface ImportTemplateDescription<FormFields> {
448401
fieldNameToTabNameMap: Record<string, string>,
449402
}
450403
451-
/*
452404
function isValidTemplate(templateName: unknown): templateName is TemplateName {
453405
const templateNameMap: Record<TemplateName, boolean> = {
454406
'dref-application': true,

app/src/utils/richText.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {
2+
isDefined,
3+
isNotDefined,
4+
} from '@togglecorp/fujs';
5+
import { type CellRichTextValue } from 'exceljs';
6+
7+
export interface ParsePlugin {
8+
tag: string,
9+
transformer: (token: string, richText: CellRichTextValue['richText'][number]) => CellRichTextValue['richText'][number],
10+
}
11+
12+
const boldPlugin: ParsePlugin = {
13+
tag: 'b',
14+
transformer: (_: string, richText) => ({
15+
...richText,
16+
font: {
17+
...richText.font,
18+
bold: true,
19+
},
20+
}),
21+
};
22+
const italicsPlugin: ParsePlugin = {
23+
tag: 'i',
24+
transformer: (_: string, richText) => ({
25+
...richText,
26+
font: {
27+
...richText.font,
28+
italic: true,
29+
},
30+
}),
31+
};
32+
const underlinePlugin: ParsePlugin = {
33+
tag: 'u',
34+
transformer: (_: string, richText) => ({
35+
...richText,
36+
font: {
37+
...richText.font,
38+
underline: true,
39+
},
40+
}),
41+
};
42+
43+
/**
44+
* Convert subset of html into excel's richtext format
45+
* @param value string with or without html tags
46+
*/
47+
export function parsePseudoHtml(
48+
value: undefined,
49+
extraPlugins?: ParsePlugin[],
50+
): undefined;
51+
export function parsePseudoHtml(
52+
value: string,
53+
extraPlugins?: ParsePlugin[],
54+
): string | CellRichTextValue
55+
export function parsePseudoHtml(
56+
value: string | undefined,
57+
extraPlugins?: ParsePlugin[],
58+
): string | CellRichTextValue | undefined
59+
export function parsePseudoHtml(
60+
value: string | undefined,
61+
extraPlugins: ParsePlugin[] = [],
62+
): string | CellRichTextValue | undefined {
63+
if (isNotDefined(value)) {
64+
return value;
65+
}
66+
67+
const plugins: ParsePlugin[] = [
68+
boldPlugin,
69+
italicsPlugin,
70+
underlinePlugin,
71+
...extraPlugins,
72+
];
73+
74+
const supportedTags = plugins.map((p) => p.tag).join('|');
75+
76+
const tagRegex = RegExp(`(</?(?:${supportedTags})>)`);
77+
const tokens = value.split(tagRegex);
78+
if (tokens.length === 1) {
79+
return value;
80+
}
81+
82+
const openTagRegex = RegExp(`<(?:${supportedTags})>`);
83+
const closeTagRegex = RegExp(`</(?:${supportedTags})>`);
84+
85+
const stack: string[] = [];
86+
const richText = tokens.map((token) => {
87+
if (token.match(openTagRegex)) {
88+
stack.push(token);
89+
return undefined;
90+
}
91+
if (token.match(closeTagRegex)) {
92+
// TODO: Check correctness by checking closeTag with last openTag
93+
stack.pop();
94+
return undefined;
95+
}
96+
97+
const applicablePlugins = plugins
98+
.filter((plugin) => stack.includes(`<${plugin.tag}>`));
99+
100+
const richTextItem: CellRichTextValue['richText'][number] = applicablePlugins
101+
.reduce(
102+
(acc, plugin) => plugin.transformer(token, acc),
103+
{ text: token },
104+
);
105+
return richTextItem;
106+
}).filter(isDefined);
107+
108+
// TODO: Check correctness to check that stack is empty
109+
return { richText };
110+
}

0 commit comments

Comments
 (0)