Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(Cross):[IOAPPX-422] Replace the legacy markdown renderer with IOMarkdown #6445

Draft
wants to merge 64 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
bdbd550
replace Markdown with IOMarkdown
adelloste Nov 12, 2024
5d8dbdd
IOMarkdown for message's preconditions
Vangaorth Nov 12, 2024
4c56ee2
Merge branch 'master' into IOAPPX-422-IOMarkdown
dmnplb Nov 22, 2024
a0aba8b
Remove legacy `Markdown` playground from Developer mode section
dmnplb Nov 22, 2024
fa53d07
Merge branch 'master' into IOAPPX-422-IOMarkdown
dmnplb Nov 22, 2024
352605c
Merge branch 'master' into IOAPPX-422-IOMarkdown
dmnplb Nov 25, 2024
3da8939
Remove `LegacyMarkdown` from `ManualConfigBottomSheet`
dmnplb Nov 25, 2024
310b798
Remove `LegacyMarkdown` from `ShareDataFeatureInfos`
dmnplb Nov 25, 2024
293a91c
Merge branch 'master' into IOAPPX-422-IOMarkdown
dmnplb Nov 25, 2024
be90854
Remove `LegacyMarkdown` from IDPay related screens
dmnplb Nov 25, 2024
0b8dc9b
Merge branch 'master' into IOAPPX-422-IOMarkdown
dmnplb Nov 25, 2024
9d404de
RootedDeviceModal
Vangaorth Nov 26, 2024
4f76da5
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Nov 26, 2024
c3ba3f8
CiePinScreen
Vangaorth Nov 26, 2024
aaac7e8
Removed unused CieAuthorizeDataUsageScreen
Vangaorth Nov 26, 2024
84d0723
ItwCiePinScreen
Vangaorth Nov 26, 2024
34e9e9e
OnboardingDescriptionMarkdown
Vangaorth Nov 26, 2024
f549582
useFciAbortSignatureFlow
Vangaorth Nov 26, 2024
0a8d9fe
EycaInformationComponent
Vangaorth Nov 26, 2024
36362aa
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Nov 26, 2024
1202df5
Removed LegacyMarkdown
Vangaorth Nov 26, 2024
358ec41
Merge branch 'master' into IOAPPX-422-IOMarkdown
dmnplb Nov 26, 2024
20acd6a
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Nov 26, 2024
78c72cf
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Nov 26, 2024
5bdfc2a
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Nov 26, 2024
acff871
Removed MessageMarkdown
Vangaorth Nov 26, 2024
23c402b
Remove MarkdownHandleCustomLink
Vangaorth Nov 26, 2024
862a62b
Tests
Vangaorth Nov 26, 2024
3ff7828
Merge branch 'master' into IOAPPX-422-IOMarkdown
dmnplb Nov 27, 2024
aa83948
Merge branch 'master' into IOAPPX-422-IOMarkdown
dmnplb Nov 29, 2024
bea148f
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Dec 5, 2024
97f699a
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Dec 5, 2024
b0d9a41
BonusInformationComponent
Vangaorth Dec 5, 2024
4f50b9f
Revert podfile
Vangaorth Dec 5, 2024
c61b510
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Dec 5, 2024
7a5385b
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Dec 9, 2024
9433d05
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Dec 17, 2024
2d27b8d
yarn.lock
Vangaorth Dec 17, 2024
9d03374
Fixes
Vangaorth Dec 17, 2024
29230e3
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Dec 18, 2024
f1535ca
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Dec 19, 2024
3112c97
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Dec 19, 2024
317f66a
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Jan 7, 2025
df7ee3a
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Jan 8, 2025
882a941
Support for Images on Android on IOMarkdown
Vangaorth Jan 8, 2025
436f733
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Jan 9, 2025
96e0056
Accessibility workaround for iOS
Vangaorth Jan 9, 2025
503dabc
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Jan 10, 2025
993405f
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Jan 20, 2025
93909cf
Minor fixes
Vangaorth Jan 20, 2025
8bca226
Fixed duplicated key warning
Vangaorth Jan 20, 2025
a8d7e40
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Jan 20, 2025
4232557
Revert removal of LegacyMarkdown
Vangaorth Jan 21, 2025
1ef8adb
Fix wrong place for xss.d.ts file
Vangaorth Jan 21, 2025
713679f
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Jan 21, 2025
e60c4e9
Revert preconditions refactoring to allow the loading state
Vangaorth Jan 21, 2025
2c68eee
Merge branch 'master' into IOAPPX-422-IOMarkdown
Vangaorth Jan 22, 2025
2187449
Add flag to enable IOMarkdown in the developer section
Vangaorth Jan 22, 2025
11ef601
Accessibility data on app reducer
Vangaorth Jan 22, 2025
459e760
Add accessibility info to IOMarkdown
Vangaorth Jan 22, 2025
f52e171
updated markdown in the detail of a service
adelloste Jan 23, 2025
862e594
Merge branch 'master' into IOAPPX-422-IOMarkdown
adelloste Jan 23, 2025
27eef5d
Old Markdown restored on message's preconditions
Vangaorth Jan 23, 2025
388014e
Restore Legacy Markdown on Message Details
Vangaorth Jan 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion ts/RootContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import * as O from "fp-ts/lib/Option";
import { pipe } from "fp-ts/lib/function";
import { PureComponent } from "react";
import {
AccessibilityInfo,
AppState,
AppStateStatus,
EmitterSubscription,
NativeEventSubscription,
StatusBar
} from "react-native";
Expand All @@ -27,6 +29,7 @@ import {
import { GlobalState } from "./store/reducers/types";
import customVariables from "./theme/variables";
import { ReactNavigationInstrumentation } from "./App";
import { setScreenReaderEnabled } from "./store/actions/preferences";

type Props = ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps & {
Expand All @@ -43,6 +46,7 @@ type Props = ReturnType<typeof mapStateToProps> &
*/
class RootContainer extends PureComponent<Props> {
private subscription: NativeEventSubscription | undefined;
private accessibilitySubscription: EmitterSubscription | undefined;
constructor(props: Props) {
super(props);
/* Configure the application to receive push notifications */
Expand All @@ -52,6 +56,11 @@ class RootContainer extends PureComponent<Props> {
private handleApplicationActivity = (activity: AppStateStatus) =>
this.props.applicationChangeState(activity);

private handleScreenReaderEnabled = (isScreenReaderEnabled: boolean) =>
this.props.setScreenReaderEnabled({
screenReaderEnabled: isScreenReaderEnabled
});

public componentDidMount() {
// boot: send the status of the application
this.handleApplicationActivity(AppState.currentState);
Expand All @@ -60,6 +69,14 @@ class RootContainer extends PureComponent<Props> {
"change",
this.handleApplicationActivity
);
// eslint-disable-next-line functional/immutable-data
this.accessibilitySubscription = AccessibilityInfo.addEventListener(
"screenReaderChanged",
this.handleScreenReaderEnabled
);
AccessibilityInfo.isScreenReaderEnabled()
.then(this.handleScreenReaderEnabled)
.catch(() => undefined);

this.updateLocale();
// Hide splash screen
Expand All @@ -80,6 +97,7 @@ class RootContainer extends PureComponent<Props> {

public componentWillUnmount() {
this.subscription?.remove();
this.accessibilitySubscription?.remove();
}

public componentDidUpdate() {
Expand Down Expand Up @@ -134,7 +152,8 @@ const mapStateToProps = (state: GlobalState) => ({
const mapDispatchToProps = {
applicationChangeState,
navigateBack,
setDebugCurrentRouteName
setDebugCurrentRouteName,
setScreenReaderEnabled
};

export default connect(mapStateToProps, mapDispatchToProps)(RootContainer);
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ exports[`Check the addition for new fields to the persisted store. If one of thi
},
"isDesignSystemEnabled": false,
"isFingerprintEnabled": undefined,
"isIOMarkdownEnabledOnMessagesAndServices": false,
"isIdPayTestEnabled": false,
"isMixpanelEnabled": null,
"isPagoPATestEnabled": false,
Expand Down
15 changes: 13 additions & 2 deletions ts/boot/configureStoreAndPersistor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import { configureReactotron } from "./configureRectotron";
/**
* Redux persist will migrate the store to the current version
*/
const CURRENT_REDUX_STORE_VERSION = 38;
const CURRENT_REDUX_STORE_VERSION = 39;

// see redux-persist documentation:
// https://github.com/rt2zz/redux-persist/blob/master/docs/migrations.md
Expand Down Expand Up @@ -455,7 +455,18 @@ const migrations: MigrationManifest = {
};
},
// Remove old wallets&payments feature and persisted state
"38": (state: PersistedState) => omit(state, "payments")
"38": (state: PersistedState) => omit(state, "payments"),
// Add 'isIOMarkdownEnabledOnMessagesAndServices' to 'persistedPreferences'
"39": (state: PersistedState) => {
const typedState = state as GlobalState;
return {
...state,
persistedPreferences: {
...typedState.persistedPreferences,
isIOMarkdownEnabledOnMessagesAndServices: false
}
};
}
};

const isDebuggingInChrome = isDevEnv && !!window.navigator.userAgent;
Expand Down
74 changes: 74 additions & 0 deletions ts/components/IOMarkdown/customRules.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Fragment } from "react";
import { TxtHeaderNode, TxtLinkNode } from "@textlint/ast-node-types";
import { Body, IOToast, MdH1, MdH2, MdH3 } from "@pagopa/io-app-design-system";
import { isIoInternalLink } from "../ui/Markdown/handlers/link";
import { handleInternalLink } from "../../utils/internalLink";
import { openWebUrl } from "../../utils/url";
import I18n from "../../i18n";
import {
generateAccesibilityLinkViewsIfNeeded,
getTxtNodeKey
} from "./renderRules";
import { IOMarkdownRenderRules, Renderer } from "./types";
import { extractAllLinksFromRootNode } from "./markdownRenderer";

const HEADINGS_MAP = {
1: MdH1,
2: MdH2,
3: MdH3,
4: Body,
5: Body,
6: Body
};

const handleOpenLink = (linkTo: (path: string) => void, url: string) => {
if (isIoInternalLink(url)) {
handleInternalLink(linkTo, url);
} else {
openWebUrl(url, () => {
IOToast.error(I18n.t("global.jserror.title"));
});
}
};

export const generateMessagesAndServicesRules = (
linkTo: (path: string) => void
): Partial<IOMarkdownRenderRules> => ({
Header(
header: TxtHeaderNode,
render: Renderer,
screenReaderEnabled: boolean
) {
const Heading = HEADINGS_MAP[header.depth];

const allLinkData = extractAllLinksFromRootNode(
header,
screenReaderEnabled
);
const nodeKey = getTxtNodeKey(header);

return (
<Fragment key={nodeKey}>
<Heading>{header.children.map(render)}</Heading>
{generateAccesibilityLinkViewsIfNeeded(
allLinkData,
nodeKey,
(url: string) => handleOpenLink(linkTo, url),
screenReaderEnabled
)}
</Fragment>
);
},
Link(link: TxtLinkNode, render: Renderer) {
return (
<Body
weight="Semibold"
asLink
key={getTxtNodeKey(link)}
onPress={() => handleOpenLink(linkTo, link.url)}
>
{link.children.map(render)}
</Body>
);
}
});
23 changes: 17 additions & 6 deletions ts/components/IOMarkdown/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { memo } from "react";
import { View } from "react-native";
import { useIOSelector } from "../../store/hooks";
import { isScreenReaderEnabledSelector } from "../../store/reducers/preferences";
import { IOMarkdownRenderRules } from "./types";
import { getRenderMarkdown, parse } from "./markdownRenderer";
import {
getRenderMarkdown,
parse,
sanitizeMarkdownForImages
} from "./markdownRenderer";
import { DEFAULT_RULES } from "./renderRules";

type Props = {
Expand All @@ -21,11 +27,16 @@ type Props = {
* It's possible to override every single rule by passing a custom `rules` object.
*/
const IOMarkdown = ({ content, rules }: Props) => {
const parsedContent = parse(content);
const renderMarkdown = getRenderMarkdown({
...DEFAULT_RULES,
...(rules || {})
});
const screenReaderEnabled = useIOSelector(isScreenReaderEnabledSelector);
const sanitizedMarkdown = sanitizeMarkdownForImages(content);
const parsedContent = parse(sanitizedMarkdown);
const renderMarkdown = getRenderMarkdown(
{
...DEFAULT_RULES,
...(rules || {})
},
screenReaderEnabled
);

return <View>{parsedContent.map(renderMarkdown)}</View>;
};
Expand Down
149 changes: 146 additions & 3 deletions ts/components/IOMarkdown/markdownRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
import { parse as textLintParse } from "@textlint/markdown-to-ast";
import { AnyTxtNode, TxtParentNode } from "@textlint/ast-node-types";
import {
AnyTxtNode,
TxtHeaderNode,
TxtLinkNode,
TxtListNode,
TxtNode,
TxtParagraphNode,
TxtParentNode,
TxtStrNode
} from "@textlint/ast-node-types";
import { omit } from "lodash";
import { isIos } from "../../utils/platform";
import { AnyTxtNodeWithSpacer, IOMarkdownRenderRules, Renderer } from "./types";

/**
*
* @param rules The `markdown` render rules.
* @returns A render function for the individual node that applies the provided rendering rules.
*/
export function getRenderMarkdown(rules: IOMarkdownRenderRules): Renderer {
export function getRenderMarkdown(
rules: IOMarkdownRenderRules,
screenReaderEnabled: boolean
): Renderer {
return (content: AnyTxtNodeWithSpacer) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
rules[content.type]?.(content, getRenderMarkdown(rules)) ?? null;
rules[content.type]?.(
content,
getRenderMarkdown(rules, screenReaderEnabled),
screenReaderEnabled
) ?? null;
}

/**
Expand Down Expand Up @@ -63,3 +80,129 @@ function integrateParent<T extends AnyTxtNode>(
}
: { ...node, parent: parentLight };
}

export const sanitizeMarkdownForImages = (
inputMarkdownContent: string
): string => {
const markdownImageRegex = /!\[.*?\]\((.*?)\)/g;

const reversedMatches: Array<RegExpExecArray> = [];
// eslint-disable-next-line functional/no-let
let match: RegExpExecArray | null;
while ((match = markdownImageRegex.exec(inputMarkdownContent)) !== null) {
// eslint-disable-next-line functional/immutable-data
reversedMatches.push(match);
}
// eslint-disable-next-line functional/immutable-data
reversedMatches.reverse();

return reversedMatches.reduce(
(sanitizedMarkdownContent, innerMatch) =>
insertNewLinesIfNeededOnMatch(sanitizedMarkdownContent, innerMatch),
inputMarkdownContent
);
};

export const insertNewLinesIfNeededOnMatch = (
markdownContent: string,
imageMatch: RegExpExecArray
): string => {
const matchStartIndex = imageMatch.index;
const matchEndIndex = matchStartIndex + imageMatch[0].length;
const sanitizedMarkdownContent = insertNewLineAtIndexIfNeeded(
markdownContent,
matchEndIndex
);
return insertNewLineAtIndexIfNeeded(
sanitizedMarkdownContent,
matchStartIndex - 1,
true
);
};

const insertNewLineAtIndexIfNeeded = (
markdownContent: string,
baseIndex: number,
insertAfterIndex: boolean = false
) => {
if (baseIndex >= 0 && baseIndex < markdownContent.length) {
const character = markdownContent[baseIndex];
if (character !== "\n") {
const index = insertAfterIndex ? baseIndex + 1 : baseIndex;
return [
markdownContent.slice(0, index),
"\n\n",
markdownContent.slice(index)
].join("");
}
}
return markdownContent;
};

export const isTxtParentNode = (node: TxtNode): node is TxtParentNode =>
node.type === "Paragraph" ||
node.type === "Header" ||
node.type === "BlockQuote" ||
node.type === "List" ||
node.type === "ListItem" ||
node.type === "Table" ||
node.type === "TableRow" ||
node.type === "TableCell" ||
node.type === "Emphasis" ||
node.type === "Strong" ||
node.type === "Delete" ||
node.type === "Link";
export const isTxtLinkNode = (node: TxtNode): node is TxtLinkNode =>
node.type === "Link";
export const isTxtStrNode = (node: TxtNode): node is TxtStrNode =>
node.type === "Str";

export type LinkData = {
text: string;
url: string;
};

export const extractAllLinksFromRootNode = (
node: TxtHeaderNode | TxtListNode | TxtParagraphNode,
screenReaderEnabled: boolean
): ReadonlyArray<LinkData> => {
const allLinkData: Array<LinkData> = [];
if (node.parent?.type === "Document" && isIos && screenReaderEnabled) {
extractAllLinksFromNodeWithChildren(node, allLinkData);
}
return allLinkData;
};

export const extractAllLinksFromNodeWithChildren = (
nodeWithChildren: Readonly<TxtParentNode>,
allLinks: Array<LinkData>
) => {
nodeWithChildren.children.forEach(node => {
if (isTxtLinkNode(node)) {
const composedLink: Array<string> = [];
extractLinkDataFromRootNode(node, composedLink);
const text = composedLink.join("");
const url = node.url;
// eslint-disable-next-line functional/immutable-data
allLinks.push({
text,
url
});
} else if (isTxtParentNode(node)) {
extractAllLinksFromNodeWithChildren(node, allLinks);
}
});
};

export const extractLinkDataFromRootNode = (
inputNode: Readonly<TxtParentNode>,
links: Array<string>
): void =>
inputNode.children.forEach(node => {
if (isTxtStrNode(node)) {
// eslint-disable-next-line functional/immutable-data
links.push(node.value);
} else if (isTxtParentNode(node)) {
extractLinkDataFromRootNode(node, links);
}
});
Loading
Loading