linkable headers
This commit is contained in:
parent
ad40092fcd
commit
beef34abfa
|
@ -77,6 +77,7 @@
|
|||
}
|
||||
|
||||
.hunk {
|
||||
overflow: visible;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 you’re 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 |
Loading…
Reference in New Issue