Skip to content

Commit

Permalink
Refactor html parsing and template import logic
Browse files Browse the repository at this point in the history
  • Loading branch information
tnagorra committed Oct 30, 2024
1 parent 26f4b7d commit 0d1f0cf
Show file tree
Hide file tree
Showing 3 changed files with 289 additions and 199 deletions.
120 changes: 36 additions & 84 deletions app/src/utils/importTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,82 +9,10 @@ import {
} from '@togglecorp/fujs';
import { type CellRichTextValue } from 'exceljs';

export function parseRichText(
value: undefined,
optionsMap?: TemplateFieldOptionsMapping,
context?: { field: string, key: string }[],
): undefined;
export function parseRichText(
value: string,
optionsMap?: TemplateFieldOptionsMapping,
context?: { field: string, key: string }[],
): string | CellRichTextValue
export function parseRichText(
value: string | undefined,
optionsMap?: TemplateFieldOptionsMapping,
context?: { field: string, key: string }[],
): string | CellRichTextValue | undefined
export function parseRichText(
value: string | undefined,
optionsMap?: TemplateFieldOptionsMapping,
context?: { field: string, key: string }[],
): string | CellRichTextValue | undefined {
if (isNotDefined(value)) {
return value;
}

const tagRegex = /(<\/?(?:b|u|i|ins)>)/;
const tokens = value.split(tagRegex);

if (tokens.length === 1) {
return value;
}

const richText:CellRichTextValue['richText'] = [];

const stack: string[] = [];

const openTagRegex = /(<(?:b|u|i|ins)>)/;
const closeTagRegex = /(<\/(?:b|u|i|ins)>)/;

tokens.forEach((token) => {
if (token.match(openTagRegex)) {
stack.push(token);
return;
}
if (token.match(closeTagRegex)) {
// TODO: Check correctness by checking closeTag with last openTag
stack.pop();
return;
}
if (stack.includes('<ins>')) {
const [optionField, valueField] = token.split('.');
const currOptions = context?.find((item) => item.field === optionField);
const selectedOption = currOptions
? optionsMap?.[optionField]?.find(
(option) => String(option.key) === currOptions?.key,
)
: undefined;

richText.push({
// FIXME: Need to add mechanism to identify if we have error for mapping
text: selectedOption?.[valueField as 'description'] ?? '',
});
} else {
richText.push({
font: {
bold: stack.includes('<b>'),
italic: stack.includes('<i>'),
underline: stack.includes('<u>'),
},
text: token,
});
}
});
// TODO: Check correctness to check that stack is empty

return { richText };
}
import {
parsePseudoHtml,
type ParsePlugin,
} from '#utils/richText';

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

export type TemplateField = HeadingTemplateField | InputTemplateField;

// TODO: add test
function createInsPlugin(
optionsMap: TemplateFieldOptionsMapping,
context: { field: string, key: string }[],
): ParsePlugin {
return {
tag: 'ins',
transformer: (token, richText) => {
const [optionField, valueField] = token.split('.');
const currOptions = context?.find((item) => item.field === optionField);
const selectedOption = currOptions
? optionsMap?.[optionField]?.find(
(option) => String(option.key) === currOptions?.key,
)
: undefined;

return {
...richText,
// FIXME: Need to add mechanism to identify if we have error for mapping
text: selectedOption?.[valueField as 'description'] ?? '',
};
},
};
}

export function createImportTemplate<
TEMPLATE_SCHEMA,
OPTIONS_MAPPING extends TemplateFieldOptionsMapping
Expand Down Expand Up @@ -269,12 +220,14 @@ export function createImportTemplate<
} satisfies HeadingTemplateField);
}

const insPlugin = createInsPlugin(optionsMap, context);

if (schema.type === 'input') {
const field = {
type: 'input',
name: fieldName,
label: parseRichText(schema.label, optionsMap, context),
description: parseRichText(schema.description, optionsMap, context),
label: parsePseudoHtml(schema.label, [insPlugin]),
description: parsePseudoHtml(schema.description, [insPlugin]),
dataValidation: (schema.validation === 'number' || schema.validation === 'date' || schema.validation === 'integer' || schema.validation === 'textArea')
? schema.validation
: undefined,
Expand All @@ -290,8 +243,8 @@ export function createImportTemplate<
const field = {
type: 'input',
name: fieldName,
label: parseRichText(schema.label, optionsMap, context),
description: parseRichText(schema.description, optionsMap, context),
label: parsePseudoHtml(schema.label, [insPlugin]),
description: parsePseudoHtml(schema.description, [insPlugin]),
outlineLevel,
dataValidation: 'list',
optionsKey: schema.optionsKey,
Expand Down Expand Up @@ -322,8 +275,8 @@ export function createImportTemplate<
context,
} satisfies HeadingTemplateField;

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

// TODO: add test
export function getValueFromImportTemplate<
TEMPLATE_SCHEMA,
OPTIONS_MAPPING extends TemplateFieldOptionsMapping,
Expand Down Expand Up @@ -437,6 +389,7 @@ export function getValueFromImportTemplate<
return listValue;
}

/*
type TemplateName = 'dref-application' | 'dref-operational-update' | 'dref-final-report';
export interface ImportTemplateDescription<FormFields> {
Expand All @@ -448,7 +401,6 @@ export interface ImportTemplateDescription<FormFields> {
fieldNameToTabNameMap: Record<string, string>,
}
/*
function isValidTemplate(templateName: unknown): templateName is TemplateName {
const templateNameMap: Record<TemplateName, boolean> = {
'dref-application': true,
Expand Down
110 changes: 110 additions & 0 deletions app/src/utils/richText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
isDefined,
isNotDefined,
} from '@togglecorp/fujs';
import { type CellRichTextValue } from 'exceljs';

export interface ParsePlugin {
tag: string,
transformer: (token: string, richText: CellRichTextValue['richText'][number]) => CellRichTextValue['richText'][number],
}

const boldPlugin: ParsePlugin = {
tag: 'b',
transformer: (_: string, richText) => ({
...richText,
font: {
...richText.font,
bold: true,
},
}),
};
const italicsPlugin: ParsePlugin = {
tag: 'i',
transformer: (_: string, richText) => ({
...richText,
font: {
...richText.font,
italic: true,
},
}),
};
const underlinePlugin: ParsePlugin = {
tag: 'u',
transformer: (_: string, richText) => ({
...richText,
font: {
...richText.font,
underline: true,
},
}),
};

/**
* Convert subset of html into excel's richtext format
* @param value string with or without html tags
*/
export function parsePseudoHtml(
value: undefined,
extraPlugins?: ParsePlugin[],
): undefined;
export function parsePseudoHtml(
value: string,
extraPlugins?: ParsePlugin[],
): string | CellRichTextValue
export function parsePseudoHtml(
value: string | undefined,
extraPlugins?: ParsePlugin[],
): string | CellRichTextValue | undefined
export function parsePseudoHtml(
value: string | undefined,
extraPlugins: ParsePlugin[] = [],
): string | CellRichTextValue | undefined {
if (isNotDefined(value)) {
return value;
}

const plugins: ParsePlugin[] = [
boldPlugin,
italicsPlugin,
underlinePlugin,
...extraPlugins,
];

const supportedTags = plugins.map((p) => p.tag).join('|');

const tagRegex = RegExp(`(</?(?:${supportedTags})>)`);
const tokens = value.split(tagRegex);
if (tokens.length === 1) {
return value;
}

const openTagRegex = RegExp(`<(?:${supportedTags})>`);
const closeTagRegex = RegExp(`</(?:${supportedTags})>`);

const stack: string[] = [];
const richText = tokens.map((token) => {
if (token.match(openTagRegex)) {
stack.push(token);
return undefined;
}
if (token.match(closeTagRegex)) {
// TODO: Check correctness by checking closeTag with last openTag
stack.pop();
return undefined;
}

const applicablePlugins = plugins
.filter((plugin) => stack.includes(`<${plugin.tag}>`));

const richTextItem: CellRichTextValue['richText'][number] = applicablePlugins
.reduce(
(acc, plugin) => plugin.transformer(token, acc),
{ text: token },
);
return richTextItem;
}).filter(isDefined);

// TODO: Check correctness to check that stack is empty
return { richText };
}
Loading

0 comments on commit 0d1f0cf

Please sign in to comment.