From 6117ce29a02155c59476d2663f75f89521959c15 Mon Sep 17 00:00:00 2001 From: Remko Date: Wed, 1 Nov 2023 16:06:52 +0100 Subject: [PATCH 1/2] added htmlparser --- .../htmlParser/alert/getAlert.module.css | 28 +++++ pwa/src/hooks/htmlParser/alert/getAlert.tsx | 34 ++++++ pwa/src/hooks/htmlParser/anchor/getAnchor.tsx | 108 ++++++++++++++++++ pwa/src/hooks/htmlParser/code/getCode.tsx | 11 ++ .../getBlockquote/getBlockquote.tsx | 6 + pwa/src/hooks/htmlParser/header/getHeader.tsx | 15 +++ pwa/src/hooks/htmlParser/image/getImage.tsx | 32 ++++++ .../hooks/htmlParser/list/getList.module.css | 3 + pwa/src/hooks/htmlParser/list/getList.tsx | 20 ++++ .../hooks/htmlParser/listItem/getListItem.tsx | 13 +++ .../htmlParser/paragraph/getParagraph.tsx | 6 + pwa/src/hooks/htmlParser/svg/getSvg.tsx | 9 ++ .../htmlParser/table/getTable.module.css | 3 + pwa/src/hooks/htmlParser/table/getTable.tsx | 11 ++ .../tableBody/getTableBody.module.css | 3 + .../htmlParser/tableBody/getTableBody.tsx | 11 ++ .../tableCell/getTableCell.module.css | 3 + .../htmlParser/tableCell/getTableCell.tsx | 11 ++ .../tableHeader/getTableHeader.module.css | 28 +++++ .../htmlParser/tableHeader/getTableHeader.tsx | 11 ++ .../tableHeaderCell/getTableHeaderCell.tsx | 6 + .../tableRow/getTableRow.module.css | 29 +++++ .../hooks/htmlParser/tableRow/getTableRow.tsx | 11 ++ pwa/src/hooks/htmlParser/useHtmlParser.ts | 98 ++++++++++++++++ 24 files changed, 510 insertions(+) create mode 100644 pwa/src/hooks/htmlParser/alert/getAlert.module.css create mode 100644 pwa/src/hooks/htmlParser/alert/getAlert.tsx create mode 100644 pwa/src/hooks/htmlParser/anchor/getAnchor.tsx create mode 100644 pwa/src/hooks/htmlParser/code/getCode.tsx create mode 100644 pwa/src/hooks/htmlParser/getBlockquote/getBlockquote.tsx create mode 100644 pwa/src/hooks/htmlParser/header/getHeader.tsx create mode 100644 pwa/src/hooks/htmlParser/image/getImage.tsx create mode 100644 pwa/src/hooks/htmlParser/list/getList.module.css create mode 100644 pwa/src/hooks/htmlParser/list/getList.tsx create mode 100644 pwa/src/hooks/htmlParser/listItem/getListItem.tsx create mode 100644 pwa/src/hooks/htmlParser/paragraph/getParagraph.tsx create mode 100644 pwa/src/hooks/htmlParser/svg/getSvg.tsx create mode 100644 pwa/src/hooks/htmlParser/table/getTable.module.css create mode 100644 pwa/src/hooks/htmlParser/table/getTable.tsx create mode 100644 pwa/src/hooks/htmlParser/tableBody/getTableBody.module.css create mode 100644 pwa/src/hooks/htmlParser/tableBody/getTableBody.tsx create mode 100644 pwa/src/hooks/htmlParser/tableCell/getTableCell.module.css create mode 100644 pwa/src/hooks/htmlParser/tableCell/getTableCell.tsx create mode 100644 pwa/src/hooks/htmlParser/tableHeader/getTableHeader.module.css create mode 100644 pwa/src/hooks/htmlParser/tableHeader/getTableHeader.tsx create mode 100644 pwa/src/hooks/htmlParser/tableHeaderCell/getTableHeaderCell.tsx create mode 100644 pwa/src/hooks/htmlParser/tableRow/getTableRow.module.css create mode 100644 pwa/src/hooks/htmlParser/tableRow/getTableRow.tsx create mode 100644 pwa/src/hooks/htmlParser/useHtmlParser.ts diff --git a/pwa/src/hooks/htmlParser/alert/getAlert.module.css b/pwa/src/hooks/htmlParser/alert/getAlert.module.css new file mode 100644 index 00000000..b5d047e4 --- /dev/null +++ b/pwa/src/hooks/htmlParser/alert/getAlert.module.css @@ -0,0 +1,28 @@ +.info svg { + fill: var(--utrecht-alert-icon-info-color); +} +.info p { + color: var(--utrecht-alert-info-color); +} + +.warning svg { + fill: var(--utrecht-alert-icon-warning-color); +} +.warning p { + color: var(--utrecht-alert-warning-color); +} + +.error svg { + fill: var(--utrecht-alert-icon-error-color); +} +.error p { + color: var(--utrecht-alert-error-color); +} + +.ok svg { + fill: var(--utrecht-alert-icon-ok-color); +} +.ok p { + color: var(--utrecht-alert-ok-color); +} + diff --git a/pwa/src/hooks/htmlParser/alert/getAlert.tsx b/pwa/src/hooks/htmlParser/alert/getAlert.tsx new file mode 100644 index 00000000..8e139885 --- /dev/null +++ b/pwa/src/hooks/htmlParser/alert/getAlert.tsx @@ -0,0 +1,34 @@ +import * as styles from "./getAlert.module.css"; +import { Alert } from "@utrecht/component-library-react/dist/css-module"; +import { domToReact } from "html-react-parser"; + +export const getAlert = (children: any, options: any, type: any) => { + switch (true) { + case type.includes("note") || type.includes("info"): + return ( + + {domToReact(children, options)} + + ); + case type.includes("error"): + return ( + + {domToReact(children, options)} + + ); + case type.includes("warning"): + return ( + + {domToReact(children, options)} + + ); + case type.includes("succes") || type.includes("ok"): + return ( + + {domToReact(children, options)} + + ); + default: + return {domToReact(children, options)}; + } +}; diff --git a/pwa/src/hooks/htmlParser/anchor/getAnchor.tsx b/pwa/src/hooks/htmlParser/anchor/getAnchor.tsx new file mode 100644 index 00000000..86de8fc5 --- /dev/null +++ b/pwa/src/hooks/htmlParser/anchor/getAnchor.tsx @@ -0,0 +1,108 @@ +import * as React from "react"; +import _ from "lodash"; +import { Link } from "@utrecht/component-library-react/dist/css-module"; +import { navigate } from "gatsby"; +import { domToReact } from "html-react-parser"; +import { TGitHubDirectory } from "../../useGitHubDirectories"; + +export const getAnchor = ( + props: any, + children: any, + options: any, + directories: TGitHubDirectory[], + location: string, +) => { + const conditions = ["://", "tel:", "mailto:"]; + const handleClick = (e: any) => { + e.preventDefault(); + + const targetFile = _.upperFirst(props.href.substring(props.href.lastIndexOf("/") + 1).replace(".md", "")); + + // No link + if (!props.href) { + navigate("#"); + + return; + } + + // Anchor Links + if (props.className === "anchor" || Array.from(props.href)[0] === "#") { + handleAnchorClick(props); // handles on-page scroll anchors + + return; + } + + // Internal Links + if (!conditions.some((substring) => props.href.includes(substring))) { + handleInternalLinks(props, targetFile, location, directories); + + return; + } + + // External Links + if (conditions.some((substring) => props.href.includes(substring))) { + open(props.href); + + return; + } + }; + + const attributes = { + ...props, + onClick: handleClick, + }; + + return {domToReact(children, options)}; +}; + +const handleInternalLinks = (props: any, targetFile: string, location: string, directories: TGitHubDirectory[]) => { + // Internal Links: same directory + if (!props.href.includes("/")) { + const targetDirectory = _.upperFirst(location.split("/").reverse()[1]); + navigate(`/pages/${targetDirectory}/${targetFile}`); + + return; // ensure no other flow is triggered + } + + // Internal Links: homepage + if (props.href.includes("/") && location === "/") { + const directoryFound = directories.some((directory) => directory.location === props.href); + + if (directoryFound) navigate(`/pages/${targetFile}`); + + return; // ensure no other flow is triggered + } + + // Internal Links: different directory + if (props.href.includes("/")) { + const targetDirectory = props.href.split("/").reverse()[1]; + const directoryFound = directories.some( + (directory) => directory.location.substring(directory.location.lastIndexOf("/") + 1) === targetDirectory, + ); + + // Internal Link exists: redirect to page + if (directoryFound) { + navigate(`/pages/${_.upperFirst(targetDirectory)}/${targetFile}`); + } + + // Internal Link does not exist: redirect to online GitHub environment (TODO) + if (!directoryFound) { + const hrefWithLeadingSlash = !props.href.startsWith("/") ? `/${props.href}` : props.href; + + open(`${process.env.GATSBY_GITHUB_REPOSITORY_URL}/blob/master${hrefWithLeadingSlash}`); + } + + return; // ensure no other flow is triggered + } +}; + +const handleAnchorClick = (props: any) => { + const targetId = props.id ?? props.href.replace("#", "user-content-"); + + const target = document.getElementById(targetId); + const headerHeight = document.getElementById("header")?.clientHeight ?? 100; + + if (target) { + window.scrollTo({ top: target.offsetTop - (headerHeight + 24), behavior: "smooth" }); // +24 simply adds some padding + } +}; diff --git a/pwa/src/hooks/htmlParser/code/getCode.tsx b/pwa/src/hooks/htmlParser/code/getCode.tsx new file mode 100644 index 00000000..77feff83 --- /dev/null +++ b/pwa/src/hooks/htmlParser/code/getCode.tsx @@ -0,0 +1,11 @@ +import { Code, CodeBlock } from "@utrecht/component-library-react/dist/css-module"; +import { domToReact } from "html-react-parser"; + +export const getCode = (name: string, props: any, children: any, options: any) => { + switch (name) { + case "code": + return {domToReact(children, options)}; + case "pre": + return {domToReact(children, options)}; + } +}; diff --git a/pwa/src/hooks/htmlParser/getBlockquote/getBlockquote.tsx b/pwa/src/hooks/htmlParser/getBlockquote/getBlockquote.tsx new file mode 100644 index 00000000..d869eb54 --- /dev/null +++ b/pwa/src/hooks/htmlParser/getBlockquote/getBlockquote.tsx @@ -0,0 +1,6 @@ +import { Alert } from "@utrecht/component-library-react/dist/css-module"; +import { domToReact } from "html-react-parser"; + +export const getBlockquote = (children: any, options: any) => { + return {domToReact(children, options)}; +}; diff --git a/pwa/src/hooks/htmlParser/header/getHeader.tsx b/pwa/src/hooks/htmlParser/header/getHeader.tsx new file mode 100644 index 00000000..61e99885 --- /dev/null +++ b/pwa/src/hooks/htmlParser/header/getHeader.tsx @@ -0,0 +1,15 @@ +import { Heading1, Heading2, Heading3, Heading4 } from "@utrecht/component-library-react/dist/css-module"; +import { domToReact } from "html-react-parser"; + +export const getHeader = (name: string, props: any, children: any, options: any) => { + switch (name) { + case "h1": + return {domToReact(children, options)}; + case "h2": + return {domToReact(children, options)}; + case "h3": + return {domToReact(children, options)}; + case "h4": + return {domToReact(children, options)}; + } +}; diff --git a/pwa/src/hooks/htmlParser/image/getImage.tsx b/pwa/src/hooks/htmlParser/image/getImage.tsx new file mode 100644 index 00000000..979ac26f --- /dev/null +++ b/pwa/src/hooks/htmlParser/image/getImage.tsx @@ -0,0 +1,32 @@ +import { Image } from "@utrecht/component-library-react/dist/css-module"; + +export const getImage = (props: any) => { + let src = props.src; + + if (!props.src.includes("https://" || "http://")) { + const sessionUrl = process.env.GATSBY_GITHUB_REPOSITORY_URL; + const url = sessionUrl?.replace("https://github.com/", ""); + + src = `https://raw.githubusercontent.com/${url}/master/docs/features/${props.src}`; + } + + let alt = props.alt; + if (!props.alt) { + alt = props.title; + } + if (!props.alt && !props.title) { + alt = props.src; + } + + const attributes = { + ...props, + src, + alt, + href: "", + onClick: (e: MouseEvent) => { + e.stopPropagation(); + open(src); + }, + }; + return ; +}; diff --git a/pwa/src/hooks/htmlParser/list/getList.module.css b/pwa/src/hooks/htmlParser/list/getList.module.css new file mode 100644 index 00000000..343a206d --- /dev/null +++ b/pwa/src/hooks/htmlParser/list/getList.module.css @@ -0,0 +1,3 @@ +.list { + width: 100% !important; +} diff --git a/pwa/src/hooks/htmlParser/list/getList.tsx b/pwa/src/hooks/htmlParser/list/getList.tsx new file mode 100644 index 00000000..fb0904f7 --- /dev/null +++ b/pwa/src/hooks/htmlParser/list/getList.tsx @@ -0,0 +1,20 @@ +import * as styles from "./getList.module.css"; +import { OrderedList, UnorderedList } from "@utrecht/component-library-react/dist/css-module"; +import { domToReact } from "html-react-parser"; + +export const getList = (name: string, props: any, children: any, options: any) => { + switch (name) { + case "ol": + return ( + + {domToReact(children, options)} + + ); + case "ul": + return ( + + {domToReact(children, options)} + + ); + } +}; diff --git a/pwa/src/hooks/htmlParser/listItem/getListItem.tsx b/pwa/src/hooks/htmlParser/listItem/getListItem.tsx new file mode 100644 index 00000000..bb71045e --- /dev/null +++ b/pwa/src/hooks/htmlParser/listItem/getListItem.tsx @@ -0,0 +1,13 @@ +import { OrderedListItem, UnorderedListItem } from "@utrecht/component-library-react/dist/css-module"; +import { domToReact } from "html-react-parser"; + +export const getListItem = (props: any, parent: any, children: any, options: any) => { + switch (parent.name) { + case "ol": + return {domToReact(children, options)}; + case "ul": + return {domToReact(children, options)}; + } + + return; +}; diff --git a/pwa/src/hooks/htmlParser/paragraph/getParagraph.tsx b/pwa/src/hooks/htmlParser/paragraph/getParagraph.tsx new file mode 100644 index 00000000..b89fc986 --- /dev/null +++ b/pwa/src/hooks/htmlParser/paragraph/getParagraph.tsx @@ -0,0 +1,6 @@ +import { Paragraph } from "@utrecht/component-library-react/dist/css-module"; +import { domToReact } from "html-react-parser"; + +export const getParagraph = (props: any, children: any, options: any) => { + return {domToReact(children, options)}; +}; diff --git a/pwa/src/hooks/htmlParser/svg/getSvg.tsx b/pwa/src/hooks/htmlParser/svg/getSvg.tsx new file mode 100644 index 00000000..cbda5358 --- /dev/null +++ b/pwa/src/hooks/htmlParser/svg/getSvg.tsx @@ -0,0 +1,9 @@ +import { domToReact } from "html-react-parser"; + +export const getSvg = (props: any, children: any, options: any) => { + if (props.className.includes("octicon octicon-link")) { + return <>; + } + + return {domToReact(children, options)}; +}; diff --git a/pwa/src/hooks/htmlParser/table/getTable.module.css b/pwa/src/hooks/htmlParser/table/getTable.module.css new file mode 100644 index 00000000..cde8a3e7 --- /dev/null +++ b/pwa/src/hooks/htmlParser/table/getTable.module.css @@ -0,0 +1,3 @@ +.table { + background-color: var(--utrecht-table-background-color) !important; +} diff --git a/pwa/src/hooks/htmlParser/table/getTable.tsx b/pwa/src/hooks/htmlParser/table/getTable.tsx new file mode 100644 index 00000000..edd8747b --- /dev/null +++ b/pwa/src/hooks/htmlParser/table/getTable.tsx @@ -0,0 +1,11 @@ +import * as styles from "./getTable.module.css"; +import { Table } from "@utrecht/component-library-react/dist/css-module"; +import { domToReact } from "html-react-parser"; + +export const getTable = (props: any, children: any, options: any) => { + return ( + + {domToReact(children, options)} +
+ ); +}; diff --git a/pwa/src/hooks/htmlParser/tableBody/getTableBody.module.css b/pwa/src/hooks/htmlParser/tableBody/getTableBody.module.css new file mode 100644 index 00000000..5c148afd --- /dev/null +++ b/pwa/src/hooks/htmlParser/tableBody/getTableBody.module.css @@ -0,0 +1,3 @@ +.tableBody { + border: var(--utrecht-table-body-border); +} diff --git a/pwa/src/hooks/htmlParser/tableBody/getTableBody.tsx b/pwa/src/hooks/htmlParser/tableBody/getTableBody.tsx new file mode 100644 index 00000000..bb81fb3b --- /dev/null +++ b/pwa/src/hooks/htmlParser/tableBody/getTableBody.tsx @@ -0,0 +1,11 @@ +import * as styles from "./getTableBody.module.css"; +import { TableBody } from "@utrecht/component-library-react/dist/css-module"; +import { domToReact } from "html-react-parser"; + +export const getTableBody = (props: any, children: any, options: any) => { + return ( + + {domToReact(children, options)} + + ); +}; diff --git a/pwa/src/hooks/htmlParser/tableCell/getTableCell.module.css b/pwa/src/hooks/htmlParser/tableCell/getTableCell.module.css new file mode 100644 index 00000000..94dc4024 --- /dev/null +++ b/pwa/src/hooks/htmlParser/tableCell/getTableCell.module.css @@ -0,0 +1,3 @@ +.tableCell { + vertical-align: middle !important; +} diff --git a/pwa/src/hooks/htmlParser/tableCell/getTableCell.tsx b/pwa/src/hooks/htmlParser/tableCell/getTableCell.tsx new file mode 100644 index 00000000..d80fc809 --- /dev/null +++ b/pwa/src/hooks/htmlParser/tableCell/getTableCell.tsx @@ -0,0 +1,11 @@ +import * as styles from "./getTableCell.module.css"; +import { TableCell } from "@utrecht/component-library-react/dist/css-module"; +import { domToReact } from "html-react-parser"; + +export const getTableCell = (props: any, children: any, options: any) => { + return ( + + {domToReact(children, options)} + + ); +}; diff --git a/pwa/src/hooks/htmlParser/tableHeader/getTableHeader.module.css b/pwa/src/hooks/htmlParser/tableHeader/getTableHeader.module.css new file mode 100644 index 00000000..59ceaddf --- /dev/null +++ b/pwa/src/hooks/htmlParser/tableHeader/getTableHeader.module.css @@ -0,0 +1,28 @@ +.tableHeader { + font-family: var(--utrecht-table-header-font-family); +} + +.tableHeader > * { + border-block-end: var(--utrecht-table-header-border-block-end-width) solid + var(--utrecht-table-header-border-block-end-color); +} + +.tableHeader > tr:nth-child(odd) { + background-color: inherit; + color: inherit; +} + +.tableHeader > tr:nth-child(odd):hover { + background-color: inherit; + color: inherit; +} + +.tableHeader > tr:nth-child(even) { + background-color: inherit; + color: inherit; +} + +.tableHeader > tr:nth-child(even):hover { + background-color: inherit; + color: inherit; +} diff --git a/pwa/src/hooks/htmlParser/tableHeader/getTableHeader.tsx b/pwa/src/hooks/htmlParser/tableHeader/getTableHeader.tsx new file mode 100644 index 00000000..2700c48f --- /dev/null +++ b/pwa/src/hooks/htmlParser/tableHeader/getTableHeader.tsx @@ -0,0 +1,11 @@ +import * as styles from "./getTableHeader.module.css"; +import { TableHeader } from "@utrecht/component-library-react/dist/css-module"; +import { domToReact } from "html-react-parser"; + +export const getTableHeader = (props: any, children: any, options: any) => { + return ( + + {domToReact(children, options)} + + ); +}; diff --git a/pwa/src/hooks/htmlParser/tableHeaderCell/getTableHeaderCell.tsx b/pwa/src/hooks/htmlParser/tableHeaderCell/getTableHeaderCell.tsx new file mode 100644 index 00000000..92ec2e6a --- /dev/null +++ b/pwa/src/hooks/htmlParser/tableHeaderCell/getTableHeaderCell.tsx @@ -0,0 +1,6 @@ +import { TableHeaderCell } from "@utrecht/component-library-react/dist/css-module"; +import { domToReact } from "html-react-parser"; + +export const getTableHeaderCell = (props: any, children: any, options: any) => { + return {domToReact(children, options)}; +}; diff --git a/pwa/src/hooks/htmlParser/tableRow/getTableRow.module.css b/pwa/src/hooks/htmlParser/tableRow/getTableRow.module.css new file mode 100644 index 00000000..2402b87a --- /dev/null +++ b/pwa/src/hooks/htmlParser/tableRow/getTableRow.module.css @@ -0,0 +1,29 @@ +.tableRow:nth-child(odd) { + background-color: var(--utrecht-table-row-alternate-odd-background-color); + color: var(--utrecht-table-row-alternate-odd-color); +} + +.tableRow:nth-child(odd):hover { + background-color: var(--utrecht-table-row-hover-background-color); + color: var(--utrecht-table-row-hover-color); +} + +.tableRow:nth-child(even) { + background-color: var(--utrecht-table-row-alternate-even-background-color); + color: var(--utrecht-table-row-alternate-even-color); +} + +.tableRow:nth-child(even):hover { + background-color: var(--utrecht-table-row-hover-background-color); + color: var(--utrecht-table-row-hover-color); +} + +.tableRow:nth-child(even) { + background-color: var(--utrecht-table-row-alternate-even-background-color); + color: var(--utrecht-table-row-alternate-even-color); +} + +.tableRow:nth-child(even):hover { + background-color: var(--utrecht-table-row-hover-background-color); + color: var(--utrecht-table-row-hover-color); +} diff --git a/pwa/src/hooks/htmlParser/tableRow/getTableRow.tsx b/pwa/src/hooks/htmlParser/tableRow/getTableRow.tsx new file mode 100644 index 00000000..41e30edf --- /dev/null +++ b/pwa/src/hooks/htmlParser/tableRow/getTableRow.tsx @@ -0,0 +1,11 @@ +import * as styles from "./getTableRow.module.css"; +import { TableRow } from "@utrecht/component-library-react/dist/css-module"; +import { domToReact } from "html-react-parser"; + +export const getTableRow = (props: any, children: any, options: any) => { + return ( + + {domToReact(children, options)} + + ); +}; diff --git a/pwa/src/hooks/htmlParser/useHtmlParser.ts b/pwa/src/hooks/htmlParser/useHtmlParser.ts new file mode 100644 index 00000000..b8d56c09 --- /dev/null +++ b/pwa/src/hooks/htmlParser/useHtmlParser.ts @@ -0,0 +1,98 @@ +import { attributesToProps } from "html-react-parser"; +import { getHeader } from "./header/getHeader"; +import { getAnchor } from "./anchor/getAnchor"; +import { getListItem } from "./listItem/getListItem"; +import { getImage } from "./image/getImage"; +import { getList } from "./list/getList"; +import { getParagraph } from "./paragraph/getParagraph"; +import { getBlockquote } from "./getBlockquote/getBlockquote"; +import { getTable } from "./table/getTable"; +import { getTableRow } from "./tableRow/getTableRow"; +import { getTableHeader } from "./tableHeader/getTableHeader"; +import { getTableHeaderCell } from "./tableHeaderCell/getTableHeaderCell"; +import { getTableBody } from "./tableBody/getTableBody"; +import { getTableCell } from "./tableCell/getTableCell"; +import { getCode } from "./code/getCode"; +import { getAlert } from "./alert/getAlert"; +import { useMarkdownDirectories } from "../useMarkdownDirectories"; +import { getSvg } from "./svg/getSvg"; + +export const useHtmlParser = (location: string) => { + const { directories } = useMarkdownDirectories(); + + const options = { + replace: ({ attribs, parent, children, name }: any) => { + if (!attribs) { + return; + } + + const props = attributesToProps(attribs); + + if (attribs && (name === "h1" || name === "h2" || name === "h3" || name === "h4")) { + return getHeader(name, props, children, options); + } + + if (attribs && name === "p") { + return getParagraph(props, children, options); + } + + if (attribs && name === "a") { + return getAnchor(props, children, options, directories, location); + } + + if (attribs && (name === "ol" || name === "ul")) { + return getList(name, props, children, options); + } + + if (attribs && name === "li") { + return getListItem(props, parent, children, options); + } + + if (attribs && name === "img") { + return getImage(props); + } + + if (attribs && name === "blockquote") { + return getBlockquote(children, options); + } + + if (attribs && name === "div" && attribs.class?.includes("markdown-alert")) { + return getAlert(children, options, attribs.class); + } + + if (attribs && name === "table") { + return getTable(props, children, options); + } + + if (attribs && name === "tr") { + return getTableRow(props, children, options); + } + + if (attribs && name === "thead") { + return getTableHeader(props, children, options); + } + + if (attribs && name === "th") { + return getTableHeaderCell(props, children, options); + } + + if (attribs && name === "tbody") { + return getTableBody(props, children, options); + } + + if (attribs && name === "td") { + return getTableCell(props, children, options); + } + + if (attribs && name === "svg") { + return getSvg(props, children, options); + } + + if (attribs && (name === "code" || name === "pre")) { + return getCode(name, props, children, options); + } + }, + }; + + return { options }; +}; From a72eeb28532a38b4560d14f24aea898fd2af42c4 Mon Sep 17 00:00:00 2001 From: Remko Date: Wed, 1 Nov 2023 16:07:51 +0100 Subject: [PATCH 2/2] added markdown page --- pwa/package-lock.json | 33 ++++++- pwa/package.json | 4 +- pwa/src/apiService/apiService.ts | 15 ++++ pwa/src/apiService/resources/markdown.ts | 17 ++++ .../ParsedHTML/ParsedHTML.module.css | 17 ++++ pwa/src/components/ParsedHTML/ParsedHTML.tsx | 85 +++++++++++++++++++ pwa/src/hooks/markdown.ts | 25 ++++++ pwa/src/hooks/useMarkdownDirectories.ts | 45 ++++++++++ pwa/src/pages/markdown/[md].tsx | 34 ++++++++ pwa/src/pages/markdown/index.tsx | 8 ++ pwa/src/services/isHtml.ts | 6 ++ .../markdown/MarkdownContentTemplate.tsx | 29 +++++++ .../templateParts/footer/FooterTemplate.tsx | 16 ++-- 13 files changed, 324 insertions(+), 10 deletions(-) create mode 100644 pwa/src/apiService/resources/markdown.ts create mode 100644 pwa/src/components/ParsedHTML/ParsedHTML.module.css create mode 100644 pwa/src/components/ParsedHTML/ParsedHTML.tsx create mode 100644 pwa/src/hooks/markdown.ts create mode 100644 pwa/src/hooks/useMarkdownDirectories.ts create mode 100644 pwa/src/pages/markdown/[md].tsx create mode 100644 pwa/src/pages/markdown/index.tsx create mode 100644 pwa/src/services/isHtml.ts create mode 100644 pwa/src/templates/markdown/MarkdownContentTemplate.tsx diff --git a/pwa/package-lock.json b/pwa/package-lock.json index 7a4ae94b..35ac365b 100644 --- a/pwa/package-lock.json +++ b/pwa/package-lock.json @@ -71,13 +71,15 @@ "react-loading-skeleton": "^3.1.0", "react-paginate": "^8.1.4", "react-query": "^3.34.19", - "react-select": "^5.3.2" + "react-select": "^5.3.2", + "showdown": "^2.1.0" }, "devDependencies": { "@types/dateformat": "^5.0.0", "@types/dedent": "^0.7.0", "@types/node": "^17.0.23", "@types/react-helmet": "^6.1.5", + "@types/showdown": "2.0.3", "@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/parser": "^5.55.0", "eslint": "^8.36.0", @@ -4112,6 +4114,12 @@ "@types/node": "*" } }, + "node_modules/@types/showdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/showdown/-/showdown-2.0.3.tgz", + "integrity": "sha512-cFuAcA3p2YPq8HR8KxvDXnOdccOZ74ypANB3kb3AL5Srji0QnteVw6vf4o7GJ8hMyz+uZ+nSQHVgXSgjYD1a5g==", + "dev": true + }, "node_modules/@types/tmp": { "version": "0.0.33", "license": "MIT" @@ -14833,6 +14841,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/showdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/showdown/-/showdown-2.1.0.tgz", + "integrity": "sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==", + "dependencies": { + "commander": "^9.0.0" + }, + "bin": { + "showdown": "bin/showdown.js" + }, + "funding": { + "type": "individual", + "url": "https://www.paypal.me/tiviesantos" + } + }, + "node_modules/showdown/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/side-channel": { "version": "1.0.4", "license": "MIT", diff --git a/pwa/package.json b/pwa/package.json index 3f08312c..ec06859d 100644 --- a/pwa/package.json +++ b/pwa/package.json @@ -87,7 +87,8 @@ "react-loading-skeleton": "^3.1.0", "react-paginate": "^8.1.4", "react-query": "^3.34.19", - "react-select": "^5.3.2" + "react-select": "^5.3.2", + "showdown": "^2.1.0" }, "devDependencies": { "@types/dateformat": "^5.0.0", @@ -96,6 +97,7 @@ "@types/react-helmet": "^6.1.5", "@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/parser": "^5.55.0", + "@types/showdown": "2.0.3", "eslint": "^8.36.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-react": "^7.32.2", diff --git a/pwa/src/apiService/apiService.ts b/pwa/src/apiService/apiService.ts index 2a4b8b6a..ba68c0e9 100644 --- a/pwa/src/apiService/apiService.ts +++ b/pwa/src/apiService/apiService.ts @@ -6,6 +6,7 @@ import { DEFAULT_FOOTER_CONTENT_URL } from "../templates/templateParts/footer/Fo // Resources import OpenWoo from "./resources/openWoo"; import FooterContent from "./resources/footerContent"; +import Markdown from "./resources/markdown"; interface PromiseMessage { loading?: string; @@ -41,13 +42,27 @@ export default class APIService { }); } + public get MarkdownClient(): AxiosInstance { + return axios.create({ + baseURL: process.env.GATSBY_BASE_URL ?? undefined, + headers: { + Accept: "application/vnd.github.html", + }, + }); + } + public get OpenWoo(): OpenWoo { return new OpenWoo(this.BaseClient, this.Send); } + public get FooterContent(): FooterContent { return new FooterContent(this.FooterContentClient, this.Send); } + public get Markdown(): Markdown { + return new Markdown(this.MarkdownClient, this.Send); + } + // Send method public Send: TSendFunction = (instance, method, endpoint, payload, promiseMessage) => { const _payload = JSON.stringify(payload); diff --git a/pwa/src/apiService/resources/markdown.ts b/pwa/src/apiService/resources/markdown.ts new file mode 100644 index 00000000..1a5a5dbb --- /dev/null +++ b/pwa/src/apiService/resources/markdown.ts @@ -0,0 +1,17 @@ +import { TSendFunction } from "../apiService"; +import { AxiosInstance } from "axios"; + +export default class Markdown { + private _instance: AxiosInstance; + private _send: TSendFunction; + constructor(_instance: AxiosInstance, send: TSendFunction) { + this._instance = _instance; + this._send = send; + } + + public getContent = async (filePath: string): Promise => { + const { data } = await this._send(this._instance, "GET", filePath); + + return data; + }; +} diff --git a/pwa/src/components/ParsedHTML/ParsedHTML.module.css b/pwa/src/components/ParsedHTML/ParsedHTML.module.css new file mode 100644 index 00000000..8b7e1d34 --- /dev/null +++ b/pwa/src/components/ParsedHTML/ParsedHTML.module.css @@ -0,0 +1,17 @@ +.container { + margin-block-start: var(--utrecht-space-block-3xl); +} + +.container > div > article > *:not(:last-child), +.backLink { + margin-block-end: var(--utrecht-space-block-lg); +} + +.backLink:hover { + cursor: pointer; +} + +.backLink { + display: flex; + align-items: center; +} diff --git a/pwa/src/components/ParsedHTML/ParsedHTML.tsx b/pwa/src/components/ParsedHTML/ParsedHTML.tsx new file mode 100644 index 00000000..18ddaeeb --- /dev/null +++ b/pwa/src/components/ParsedHTML/ParsedHTML.tsx @@ -0,0 +1,85 @@ +import * as React from "react"; +import * as styles from "./ParsedHTML.module.css"; +import Parser from "html-react-parser"; +import Skeleton from "react-loading-skeleton"; +import clsx from "clsx"; +import showdown from "showdown"; +import { Alert } from "@utrecht/component-library-react/dist/css-module"; +import { UseQueryResult } from "react-query"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faArrowLeft, faWarning } from "@fortawesome/free-solid-svg-icons"; +import { useHtmlParser } from "../../hooks/htmlParser/useHtmlParser"; +import { isHtml } from "../../services/isHtml"; +import { Link } from "@utrecht/component-library-react/dist/css-module"; +import { navigate } from "gatsby"; +import { useTranslation } from "react-i18next"; + +interface ParsedHTMLProps { + contentQuery: UseQueryResult; + location: string; + layoutClassName?: string; +} + +export const ParsedHTML: React.FC = ({ contentQuery, location, layoutClassName }) => { + const { t } = useTranslation(); + const { options } = useHtmlParser(location); + let htmlContent; + + showdown.setFlavor("github"); + + if (!isHtml(contentQuery.data)) { + const converter = new showdown.Converter(); + htmlContent = `
${converter.makeHtml( + contentQuery.data, + )}
`; + } + if (isHtml(contentQuery.data)) { + htmlContent = contentQuery.data; + } + + if (contentQuery.isLoading) + return ( +
+ +
+ ); + + if (contentQuery.isError) + return ( +
+
+ { + e.preventDefault(), navigate("/"); + }} + tabIndex={0} + > + {t("Back to homepage")} + +
+ } type="error"> + Oops, something went wrong retrieving the .md file from GitHub. + +
+ ); + + return ( +
+
+ { + e.preventDefault(), navigate("/"); + }} + tabIndex={0} + > + {t("Back to homepage")} + +
+ {Parser(htmlContent, options)} +
+ ); +}; diff --git a/pwa/src/hooks/markdown.ts b/pwa/src/hooks/markdown.ts new file mode 100644 index 00000000..33c151b8 --- /dev/null +++ b/pwa/src/hooks/markdown.ts @@ -0,0 +1,25 @@ +import * as React from "react"; +import { useQuery } from "react-query"; +import APIService from "../apiService/apiService"; +import APIContext from "../apiService/apiContext"; + +export const useMarkdown = () => { + const API: APIService | null = React.useContext(APIContext); + + const getContent = (filePath: string) => + useQuery({ + queryKey: ["contents", filePath], + queryFn: () => API?.Markdown.getContent(filePath), + onError: (error) => { + console.warn(error.message); + }, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + retry: 1, + retryDelay: 2000, + staleTime: 1000 * 60 * 60, // one hour + }); + + return { getContent }; +}; diff --git a/pwa/src/hooks/useMarkdownDirectories.ts b/pwa/src/hooks/useMarkdownDirectories.ts new file mode 100644 index 00000000..107afcf6 --- /dev/null +++ b/pwa/src/hooks/useMarkdownDirectories.ts @@ -0,0 +1,45 @@ +import * as React from "react"; + +export type TMarkdownDirectory = { + name: string; + location: string; +}; + +export const useMarkdownDirectories = () => { + const [directories, setDirectories] = React.useState([]); + + React.useEffect(() => { + const markdownDirectoryPathsString: string | undefined = process.env.GATSBY_GITHUB_DOCS_DIRECTORY_PATHS; + + if (!markdownDirectoryPathsString) return; + + try { + const directories = JSON.parse(markdownDirectoryPathsString); + + setDirectories(directories); + } catch { + console.warn("Something went wrong parsing the Markdown directories."); + } + }, []); + + const getSlugFromName = (name: string): string => name?.replace(" ", "-"); + const getNameFromSlug = (slug: string): string => slug?.replace("-", " "); // internal function + + const getDirectoryReadMeLocation = (pageSlug: string): string => { + const directory = directories.find((directory) => directory.name === getNameFromSlug(pageSlug)); + + if (!directory) return ""; + + return `${directory.location}/README.md`; + }; + + const getDetailMdLocation = (pageSlug: string, detailPageSlug: string): string => { + const directory = directories.find((directory) => directory.name === getNameFromSlug(pageSlug)); + + if (!directory) return ""; + + return `${directory.location}/${getNameFromSlug(detailPageSlug)}.md`; + }; + + return { directories, getSlugFromName, getDirectoryReadMeLocation, getDetailMdLocation }; +}; diff --git a/pwa/src/pages/markdown/[md].tsx b/pwa/src/pages/markdown/[md].tsx new file mode 100644 index 00000000..b9ce67a0 --- /dev/null +++ b/pwa/src/pages/markdown/[md].tsx @@ -0,0 +1,34 @@ +import * as React from "react"; +import qs from "qs"; +import { PageProps } from "gatsby"; +import { MarkdownContentTemplate } from "../../templates/markdown/MarkdownContentTemplate"; +import { useGatsbyContext } from "../../context/gatsby"; +import { useTranslation } from "react-i18next"; +import { Page, PageContent } from "@utrecht/component-library-react/dist/css-module"; + +const MarkdownPage: React.FC = (props: PageProps) => { + const { t } = useTranslation(); + const { gatsbyContext } = useGatsbyContext(); + + const url = gatsbyContext.location.search; + const [, params] = url.split("?"); + const parsedParams = qs.parse(params); + const link = parsedParams.link?.toString(); + + const detailPageSlug = props.params.detailPageSlug; + const pageSlug = props.params.pageSlug; + + if (!link) { + return {t("No markdown file found, make sure that the query param link is filled")}; + } + + return ( + + + + + + ); +}; + +export default MarkdownPage; diff --git a/pwa/src/pages/markdown/index.tsx b/pwa/src/pages/markdown/index.tsx new file mode 100644 index 00000000..b8424267 --- /dev/null +++ b/pwa/src/pages/markdown/index.tsx @@ -0,0 +1,8 @@ +import { navigate } from "gatsby"; + +const IndexPage = () => { + navigate("/"); + return <>; +}; + +export default IndexPage; diff --git a/pwa/src/services/isHtml.ts b/pwa/src/services/isHtml.ts new file mode 100644 index 00000000..fdc532ed --- /dev/null +++ b/pwa/src/services/isHtml.ts @@ -0,0 +1,6 @@ +//This function checks if the string contains HTML code +export const isHtml = (data: string): boolean => { + const hmlRegex = /<([A-Za-z][A-Za-z0-9]*)\b[^>]*>(.*?)<\/\1>/; + + return hmlRegex.test(data); +}; diff --git a/pwa/src/templates/markdown/MarkdownContentTemplate.tsx b/pwa/src/templates/markdown/MarkdownContentTemplate.tsx new file mode 100644 index 00000000..e72536a7 --- /dev/null +++ b/pwa/src/templates/markdown/MarkdownContentTemplate.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import { useMarkdown } from "../../hooks/markdown"; +import { ParsedHTML } from "../../components/ParsedHTML/ParsedHTML"; +import { useMarkdownDirectories } from "../../hooks/useMarkdownDirectories"; + +interface MarkdownContentTemplateProps { + pageSlug: string; + detailPageSlug: string; + link: string; +} + +export const MarkdownContentTemplate: React.FC = ({ pageSlug, detailPageSlug, link }) => { + const { getDetailMdLocation } = useMarkdownDirectories(); + + const location = getDetailMdLocation(pageSlug, detailPageSlug); + + let content: any; + + if (link.includes("https://github.com/")) { + const linkHttps = link.replace("https://github.com/", "https://api.github.com/repos/"); + linkHttps.includes("/blob/main/") + ? (content = useMarkdown().getContent(linkHttps.replace("/blob/main/", "/contents/"))) + : (content = useMarkdown().getContent(linkHttps.replace("/blob/master/", "/contents/"))); + } else { + content = useMarkdown().getContent(link.includes("https://api.github.com/repos/") ? link : link); + } + + return ; +}; diff --git a/pwa/src/templates/templateParts/footer/FooterTemplate.tsx b/pwa/src/templates/templateParts/footer/FooterTemplate.tsx index 39549410..358af4da 100644 --- a/pwa/src/templates/templateParts/footer/FooterTemplate.tsx +++ b/pwa/src/templates/templateParts/footer/FooterTemplate.tsx @@ -48,15 +48,15 @@ export const FooterTemplate: React.FC = () => { const getFooterContent = _useFooterContent.getContent(); // For production - React.useEffect(() => { - setFooterContent(getFooterContent.data); - }, [getFooterContent]); + // React.useEffect(() => { + // setFooterContent(getFooterContent.data); + // }, [getFooterContent]); // For development - // React.useEffect(() => { - // const data = require("./FooterContent.json"); - // setFooterContent(data); - // }, []); + React.useEffect(() => { + const data = require("./FooterContent.json"); + setFooterContent(data); + }, []); React.useEffect(() => { if (!process.env.GATSBY_FOOTER_CONTENT) return; @@ -269,7 +269,7 @@ const MarkdownLink: React.FC = ({ item }) => { { - e.preventDefault(), navigate(`/${item.value.replaceAll(" ", "_")}/?link=${item.markdownLink}`); + e.preventDefault(), navigate(`/markdown/${item.value.replaceAll(" ", "_")}/?link=${item.markdownLink}`); }} tabIndex={0} aria-label={`${t(item.ariaLabel)}, ${t(item.markdownLink)}`}