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

Hierarchical markdown #193

Merged
merged 7 commits into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 0 additions & 3 deletions .yarn/versions/5766cc7b.yml

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
},
"devDependencies": {
"@essex/eslint-config": "^20.5.1",
"@essex/scripts": "^24.0.1",
"@essex/scripts": "^24.0.2",
"@types/jest": "^29.2.6",
"@types/node": "^18.11.18",
"@types/react": "^18.0.27",
Expand Down
2 changes: 1 addition & 1 deletion packages/boolean-expression-component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"release": "yarn npm publish --tolerate-republish --access public"
},
"devDependencies": {
"@essex/scripts": "^24.0.1",
"@essex/scripts": "^24.0.2",
"@fluentui/react": "^8.104.6",
"@mdx-js/react": "^1.6.22",
"@storybook/addon-docs": "^6.5.15",
Expand Down
2 changes: 1 addition & 1 deletion packages/charts-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@essex/scripts": "^24.0.1",
"@essex/scripts": "^24.0.2",
"@essex/tsconfig-base": "^1.0.2",
"@mdx-js/react": "^1.6.22",
"@storybook/addon-docs": "^6.5.15",
Expand Down
4 changes: 2 additions & 2 deletions packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@essex/components",
"version": "3.6.8",
"version": "3.6.9",
"description": "A set of visual components that include built-in thematic",
"type": "module",
"main": "src/index.ts",
Expand Down Expand Up @@ -40,7 +40,7 @@
"react-if": "^4.1.4"
},
"devDependencies": {
"@essex/scripts": "^24.0.1",
"@essex/scripts": "^24.0.2",
"@fluentui/react": "^8.104.6",
"@mdx-js/react": "^1.6.22",
"@storybook/addon-docs": "^6.5.15",
Expand Down
109 changes: 85 additions & 24 deletions packages/components/src/MarkdownBrowser/MarkdownBrowser.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,41 +35,36 @@ export function useHistory(home?: string): {
}
}

/**
* Override link click behavior to intercept relative links.
* @param container
* @param goForward
* @param current
*/
export function useLinkNavigation(
container: React.MutableRefObject<HTMLDivElement | null>,
parent: string,
href: string,
goForward: (to: string) => void,
current: string | undefined,
) {
const onLinkClick = useCallback(
(url: string) => {
return useCallback(
(
event: React.MouseEvent<
HTMLAnchorElement | HTMLButtonElement | HTMLElement
>,
) => {
event.preventDefault()
// if the link is not relative, open in a new window
if (!url.includes(window.location.origin)) {
return window.open(url, '_blank')
if (isExternalLink(href)) {
return window.open(href, '_blank')
}
// otherwise, navigate to the relative link
// TODO: this only supports last path segment
// we could potentially provide better support for nested documentation paths
const name = url.split('/').pop()?.replace(/.md/, '')
const name = parseRelativePath(href, parent)
if (name) {
goForward(name)
}
},
[goForward],
[parent, href, goForward],
)
// override link click behavior to intercept relative links
// note the inclusion of the current value in the deps to trigger re-attachment of handlers
// because refs don't trigger re-renders
useEffect(() => {
if (container?.current) {
const links = container.current.querySelectorAll('a')
links.forEach((link: any) => {
link.addEventListener('click', (e: any) => {
e.preventDefault()
onLinkClick((e.target as HTMLAnchorElement).href)
})
})
}
}, [onLinkClick, container, current])
}

export function useIconButtonProps(
Expand All @@ -93,3 +88,69 @@ export function useIconButtonProps(
)
}, [styles, iconName, onClick, overrides])
}

/**
* Construct the props for an icon
* specific to external links.
* @param url
*/
export function useLinkIconProps(url: string) {
return useMemo(
() => ({
styles: {
root: {
marginLeft: 2,
fontSize: '0.8em',
width: '0.8em',
height: '0.8em',
},
},
iconName: 'NavigateExternalInline',
// we have to provide separate click handling for the icon
onClick: () => window.open(url, '_blank'),
}),
[url],
)
}

// We have to do a little housekeeping on the paths to navigate relative content
// The content must use "." to separate paths in order to be JS-compliant,
// we want to look for nested paths and align them with the parent
// to ensure the entire structure remains intact as a key into the content index
function parseRelativePath(path: string, parent: string) {
const relative = path.replace(window.location.origin, '').replace(/.md/, '')
const parts = relative.split('/')
const parentParts = parent.split(/\./g)

// sibling, push it into the same "folder"
if (parts[0] === '.') {
return [
...parentParts.slice(0, parentParts.length - 1),
...parts.slice(1),
].join('.')
}

// if it's nested deeper, slice out the correct number of levels
const levels = parts.filter((p) => p === '..').length
if (levels > 0) {
return [
...parentParts.slice(0, parentParts.length - (levels + 1)),
...parts.slice(levels),
].join('.')
}

// fallback for unaccounted for path structures
return relative.replace(/\//g, '.')
}

/**
* Relative paths should either include the origin or have no protocol.
* @param url
* @returns
*/
export function isExternalLink(url: string) {
if (url.includes(':')) {
return !url.includes(window.location.origin)
}
return false
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,21 @@ Link to [groupby](./groupby.md) and [fill](./fill.md) to support all-in-one data
| 2 | 11 |
| 2 | 18 |

\`aggregate column['value'] with function='sum', groupby=column['id'], t_column='output'\`:
[no header](./noheader.md)

| id | output |
| --- | ------ |
| 1 | 25 |
| 2 | 30 |
We can also link to [external](https://en.wikipedia.org/wiki/Markdown) content.

[no header](./noheader.md)
And we can handle [nested relative content](./nested/content.md)
`,

groupby: `
# groupby

Groups table rows using keys from a specified column list. Note that this is an underlying index on a table that isn't necessarily visible, but will apply when performing operations that are sensitive to grouping. See [aggregate](./aggregate.md) for examples of \`groupby\`.

Here is a [missing link](./missing.md).
`,

fill: `
# fill

Expand All @@ -69,6 +68,7 @@ Creates a new output column and fills it with a fixed value.
| 3 | hi |

`,

noheader: `
This content has no header so we can see how the alignment works with the navigation buttons

Expand All @@ -91,6 +91,29 @@ Here is a [missing link](./missing.md).
| 3 | hi |

`,

'nested.content': `
This is a nested content file.

Link to [sibling](./sibling.md).

Link back to [aggregate](../aggregate.md)`,

'nested.sibling': `
This is a nested sibling content file.

Link to [nested](./content.md).

Link to [deeply nested](./child/leveltwo.md).

Link back to [aggregate](../aggregate.md)`,

'nested.child.leveltwo': `
This is a deeply nested content file for relative parent folder testing.

Link to [nested parent](../content.md).

Link back up to [aggregate](../../aggregate.md)`,
}

const Template: ComponentStory<typeof MarkdownBrowser> = (
Expand Down
44 changes: 39 additions & 5 deletions packages/components/src/MarkdownBrowser/MarkdownBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
import { IconButton } from '@fluentui/react'
import { memo, useRef } from 'react'
import { Icon, IconButton, Link } from '@fluentui/react'
import type { PropsWithChildren } from 'react'
import { memo, useMemo, useRef } from 'react'

import {
isExternalLink,
useHistory,
useIconButtonProps,
useLinkIconProps,
useLinkNavigation,
} from './MarkdownBrowser.hooks.js'
import {
Expand Down Expand Up @@ -36,8 +39,7 @@ export const MarkdownBrowser: React.FC<MarkdownBrowserProps> = memo(
const container = useRef<HTMLDivElement>(null)
const { current, goHome, goBack, goForward } = useHistory(home)

useLinkNavigation(container, goForward, current)

const options = useMarkdownOptions(current, goForward)
// fallback to empty string - markdown component will fail if content is undefined
const md = current ? content[current] : ''

Expand All @@ -50,9 +52,41 @@ export const MarkdownBrowser: React.FC<MarkdownBrowserProps> = memo(
{goHome && <IconButton {...homeProps} />}
</Navigation>
{md && (
<MarkdownContainer style={styles.markdown}>{md}</MarkdownContainer>
<MarkdownContainer options={options} style={styles.markdown}>
{md}
</MarkdownContainer>
)}
</Container>
)
},
)

const A = (props: PropsWithChildren<any>) => {
const { children, href, current, goForward, ...rest } = props
const isExternal = isExternalLink(href)
const iconProps = useLinkIconProps(href)
const onClick = useLinkNavigation(current, href, goForward)
return (
<Link href={href} onClick={onClick} {...rest}>
{children}
{isExternal && <Icon {...iconProps} />}
</Link>
)
}

function useMarkdownOptions(current: string | undefined, goForward: any) {
return useMemo(
() => ({
overrides: {
a: {
component: A,
props: {
current,
goForward,
},
},
},
}),
[current, goForward],
)
}
2 changes: 1 addition & 1 deletion packages/graphql-api-commons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
},
"devDependencies": {
"@apollo/server": "^4.3.1",
"@essex/scripts": "^24.0.1",
"@essex/scripts": "^24.0.2",
"@graphql-tools/utils": "^9.1.4",
"@tsconfig/node14": "^1.0.3",
"fastify": "^4.12.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/hierarchy-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"styled-components": "^5.3.6"
},
"devDependencies": {
"@essex/scripts": "^24.0.1",
"@essex/scripts": "^24.0.2",
"@fluentui/react": "^8.104.6",
"@mdx-js/react": "^1.6.22",
"@storybook/addon-docs": "^6.5.15",
Expand Down
2 changes: 1 addition & 1 deletion packages/hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
},
"devDependencies": {
"@essex/jest-config": "^21.0.20",
"@essex/scripts": "^24.0.1",
"@essex/scripts": "^24.0.2",
"@testing-library/react": "^13.4.0",
"@types/jest": "^29.2.6",
"@types/lodash-es": "^4.17.6",
Expand Down
2 changes: 1 addition & 1 deletion packages/msal-interactor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
},
"devDependencies": {
"@azure/msal-browser": "^2.32.2",
"@essex/scripts": "^24.0.1",
"@essex/scripts": "^24.0.2",
"@types/debug": "^4.1.7"
},
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/semantic-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
},
"devDependencies": {
"@essex/jest-config": "^21.0.20",
"@essex/scripts": "^24.0.1",
"@essex/scripts": "^24.0.2",
"@essex/tsconfig-base": "^1.0.2",
"@testing-library/react": "^13.4.0",
"@types/react": "^18.0.27",
Expand Down
4 changes: 2 additions & 2 deletions packages/stories/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "essex-toolkit-stories",
"private": true,
"version": "3.7.10",
"version": "3.7.11",
"description": "A set of visual components that include built-in thematic",
"repository": "https://github.com/microsoft/essex-toolkit",
"author": "Nathan Evans <naevans@microsoft.com>",
Expand Down Expand Up @@ -31,7 +31,7 @@
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@essex/jest-config": "^21.0.20",
"@essex/scripts": "^24.0.1",
"@essex/scripts": "^24.0.2",
"@essex/storybook-config": "^0.0.3",
"@fluentui/font-icons-mdl2": "^8.5.6",
"@fluentui/react": "^8.104.6",
Expand Down
2 changes: 1 addition & 1 deletion packages/styled-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"release": "yarn npm publish --tolerate-republish --access public"
},
"devDependencies": {
"@essex/scripts": "^24.0.1",
"@essex/scripts": "^24.0.2",
"@essex/tsconfig-base": "^1.0.2",
"@types/styled-components": "^5.1.26",
"react": "^18.2.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/thematic-lineup/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"tslib": "^2.4.1"
},
"devDependencies": {
"@essex/scripts": "^24.0.1",
"@essex/scripts": "^24.0.2",
"@mdx-js/react": "^1.6.22",
"@storybook/addon-docs": "^6.5.15",
"@thematic/color": "^4.0.4",
Expand Down
Loading