linkable headers

This commit is contained in:
keyan 2022-07-17 10:33:55 -05:00
parent ad40092fcd
commit beef34abfa
7 changed files with 81 additions and 23 deletions

View File

@ -77,6 +77,7 @@
}
.hunk {
overflow: visible;
margin-bottom: 0;
margin-top: 0.15rem;
}

View File

@ -103,7 +103,7 @@ export function MarkdownInput ({ label, topLevel, groupClassName, ...props }) {
</div>
<div className={tab !== 'preview' ? 'd-none' : 'form-group'}>
<div className={`${styles.text} form-control`}>
{tab === 'preview' && <Text topLevel={topLevel}>{meta.value}</Text>}
{tab === 'preview' && <Text topLevel={topLevel} noFragments>{meta.value}</Text>}
</div>
</div>
</div>
@ -254,11 +254,13 @@ export function Form ({
{error && <Alert variant='danger' onClose={() => setError(undefined)} dismissible>{error}</Alert>}
{storageKeyPrefix
? React.Children.map(children, (child) => {
// if child has a type it's a dom element
// if child has a type that's a string, it's a dom element and can't get a prop
if (child) {
return React.cloneElement(child, {
storageKeyPrefix
})
let childProps = {}
if (typeof child.type !== 'string') {
childProps = { storageKeyPrefix }
}
return React.cloneElement(child, childProps)
}
})
: children}

View File

@ -2,14 +2,15 @@ import styles from './text.module.css'
import ReactMarkdown from 'react-markdown'
import gfm from 'remark-gfm'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
/* Use `…/dist/cjs/…` if youre not in ESM! */
import { atomDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
import mention from '../lib/remark-mention'
import sub from '../lib/remark-sub'
import remarkDirective from 'remark-directive'
import { visit } from 'unist-util-visit'
import reactStringReplace from 'react-string-replace'
import { useEffect, useState } from 'react'
import React, { useEffect, useState } from 'react'
import GithubSlugger from 'github-slugger'
import Link from '../svgs/link.svg'
function myRemarkPlugin () {
return (tree) => {
@ -28,19 +29,41 @@ function myRemarkPlugin () {
}
}
export default function Text ({ topLevel, nofollow, children }) {
function Heading ({ h, slugger, noFragments, topLevel, children, node, ...props }) {
const id = noFragments
? undefined
: slugger.slug(children.reduce(
(acc, cur) => {
if (typeof cur !== 'string') {
return acc
}
return acc + cur.replace(/[^\w\-\s]+/gi, '')
}, ''))
return (
<div className={styles.heading}>
{React.createElement(h, { id, ...props }, children)}
{!noFragments && topLevel && <a className={styles.headingLink} href={`#${id}`}><Link width={18} height={18} className='fill-grey' /></a>}
</div>
)
}
export default function Text ({ topLevel, noFragments, nofollow, children }) {
// all the reactStringReplace calls are to facilitate search highlighting
const slugger = new GithubSlugger()
const HeadingWrapper = (props) => Heading({ topLevel, slugger, noFragments, ...props})
return (
<div className={styles.text}>
<ReactMarkdown
components={{
h1: ({children, node, ...props}) => topLevel ? <h1 {...props}>{children}</h1> : <h3 {...props}>{children}</h3>,
h2: ({children, node, ...props}) => topLevel ? <h2 {...props}>{children}</h2> : <h4 {...props}>{children}</h4>,
h3: ({children, node, ...props}) => topLevel ? <h3 {...props}>{children}</h3> : <h5 {...props}>{children}</h5>,
h4: ({children, node, ...props}) => topLevel ? <h4 {...props}>{children}</h4> : <h6 {...props}>{children}</h6>,
h5: ({children, node, ...props}) => topLevel ? <h5 {...props}>{children}</h5> : <h6 {...props}>{children}</h6>,
h6: 'h6',
h1: (props) => HeadingWrapper({ h: topLevel ? 'h1' : 'h3', ...props }),
h2: (props) => HeadingWrapper({ h: topLevel ? 'h2' : 'h4', ...props }),
h3: (props) => HeadingWrapper({ h: topLevel ? 'h3' : 'h5', ...props }),
h4: (props) => HeadingWrapper({ h: topLevel ? 'h4' : 'h6', ...props }),
h5: (props) => HeadingWrapper({ h: topLevel ? 'h5' : 'h6', ...props }),
h6: (props) => HeadingWrapper({ h: 'h6', ...props }),
table: ({ node, ...props }) =>
<div className='table-responsive'>
<table className='table table-bordered table-sm' {...props} />
@ -64,7 +87,8 @@ export default function Text ({ topLevel, nofollow, children }) {
)
},
a: ({ node, href, children, ...props }) => {
children = children?.map(e => typeof e === 'string'
children = children?.map(e =>
typeof e === 'string'
? reactStringReplace(e, /:high\[([^\]]+)\]/g, (match, i) => {
return <mark key={`mark-${match}-${i}`}>{match}</mark>
})
@ -82,7 +106,7 @@ export default function Text ({ topLevel, nofollow, children }) {
</a>
)
},
img: ({node, ...props}) => <ZoomableImage topLevel={topLevel} {...props}/>
img: ({ node, ...props }) => <ZoomableImage topLevel={topLevel} {...props} />
}}
remarkPlugins={[gfm, mention, sub, remarkDirective, myRemarkPlugin]}
>
@ -119,11 +143,13 @@ function ZoomableImage ({ src, topLevel, ...props }) {
}
}
return <img
className={topLevel ? styles.topLevel : undefined}
style={mediaStyle}
src={src}
onClick={handleClick}
{...props}
/>
return (
<img
className={topLevel ? styles.topLevel : undefined}
style={mediaStyle}
src={src}
onClick={handleClick}
{...props}
/>
)
}

View File

@ -11,6 +11,28 @@
}
}
.heading {
position: relative;
margin-left: -22px;
padding-left: 22px;
}
.headingLink {
display: none;
position: absolute;
left: 0px;
top: 0px;
height: 100%;
}
.headingLink svg {
align-self: center;
}
.heading:hover>.headingLink {
display: flex;
}
.text hr {
border-top: 1px solid var(--theme-clickToContextColor);
}

5
package-lock.json generated
View File

@ -3912,6 +3912,11 @@
"get-intrinsic": "^1.1.1"
}
},
"github-slugger": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.4.0.tgz",
"integrity": "sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ=="
},
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",

View File

@ -23,6 +23,7 @@
"cross-fetch": "^3.1.5",
"domino": "^2.1.6",
"formik": "^2.2.6",
"github-slugger": "^1.4.0",
"graphql": "^15.5.0",
"graphql-type-json": "^0.3.2",
"ln-service": "^52.8.0",

1
svgs/link.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18.364 15.536L16.95 14.12l1.414-1.414a5 5 0 1 0-7.071-7.071L9.879 7.05 8.464 5.636 9.88 4.222a7 7 0 0 1 9.9 9.9l-1.415 1.414zm-2.828 2.828l-1.415 1.414a7 7 0 0 1-9.9-9.9l1.415-1.414L7.05 9.88l-1.414 1.414a5 5 0 1 0 7.071 7.071l1.414-1.414 1.415 1.414zm-.708-10.607l1.415 1.415-7.071 7.07-1.415-1.414 7.071-7.07z"/></svg>

After

Width:  |  Height:  |  Size: 450 B