mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-12-23 17:04:18 +00:00
Added TOC and anchors for headings (#243)
* Added TOC support and anchors for headings * Moved @types/markdown-it-anchor from devDependencies to dependencies * Add subnode renderer Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> * Added node-replacer for toc generation ul lists may not be nested inside a p element. Therefore replaces this replacer every p that has a div.table-of-contents inside of it with the div directly. * Add index to replacer function Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> * Add TOC to example code Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> * Remove unused import Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> * Removed unnecessary div wrapper of toc * Fixed toc-renderer Co-authored-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
cb2ea5fa6e
commit
fd378cf89b
12 changed files with 93 additions and 13 deletions
|
@ -9,6 +9,7 @@
|
|||
"@types/codemirror": "0.0.96",
|
||||
"@types/jest": "26.0.0",
|
||||
"@types/markdown-it": "10.0.1",
|
||||
"@types/markdown-it-anchor": "4.0.4",
|
||||
"@types/markdown-it-container": "2.0.3",
|
||||
"@types/node": "12.12.47",
|
||||
"@types/node-sass": "4.11.1",
|
||||
|
@ -40,15 +41,17 @@
|
|||
"i18next-http-backend": "1.0.15",
|
||||
"markdown-it": "11.0.0",
|
||||
"markdown-it-abbr": "1.0.4",
|
||||
"markdown-it-deflist": "2.0.3",
|
||||
"markdown-it-anchor": "5.3.0",
|
||||
"markdown-it-container": "3.0.0",
|
||||
"markdown-it-deflist": "2.0.3",
|
||||
"markdown-it-emoji": "1.4.0",
|
||||
"markdown-it-footnote": "3.0.2",
|
||||
"markdown-it-ins": "3.0.0",
|
||||
"markdown-it-mark": "3.0.0",
|
||||
"markdown-it-footnote": "3.0.2",
|
||||
"markdown-it-regex": "0.2.0",
|
||||
"markdown-it-sub": "1.0.0",
|
||||
"markdown-it-sup": "1.0.0",
|
||||
"markdown-it-table-of-contents": "0.4.4",
|
||||
"markdown-it-task-lists": "2.1.1",
|
||||
"moment": "2.27.0",
|
||||
"node-sass": "4.14.1",
|
||||
|
|
|
@ -12,8 +12,9 @@ import { TaskBar } from './task-bar/task-bar'
|
|||
|
||||
const Editor: React.FC = () => {
|
||||
const editorMode: EditorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
|
||||
const [markdownContent, setMarkdownContent] = useState(`
|
||||
# Embedding demo
|
||||
const [markdownContent, setMarkdownContent] = useState(`# Embedding demo
|
||||
[TOC]
|
||||
|
||||
## Slideshare
|
||||
{%slideshare mazlan1/internet-of-things-the-tip-of-an-iceberg %}
|
||||
|
||||
|
|
|
@ -7,4 +7,15 @@
|
|||
.alert > p, .alert > ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
a.heading-anchor {
|
||||
margin-left: -1.25em;
|
||||
font-size: 0.75em;
|
||||
margin-top: 0.25em;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { DomElement } from 'domhandler'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import abbreviation from 'markdown-it-abbr'
|
||||
import anchor from 'markdown-it-anchor'
|
||||
import markdownItContainer from 'markdown-it-container'
|
||||
import definitionList from 'markdown-it-deflist'
|
||||
import emoji from 'markdown-it-emoji'
|
||||
|
@ -10,6 +11,7 @@ import marked from 'markdown-it-mark'
|
|||
import markdownItRegex from 'markdown-it-regex'
|
||||
import subscript from 'markdown-it-sub'
|
||||
import superscript from 'markdown-it-sup'
|
||||
import toc from 'markdown-it-table-of-contents'
|
||||
import taskList from 'markdown-it-task-lists'
|
||||
import React, { ReactElement, useMemo } from 'react'
|
||||
import ReactHtmlParser, { convertNodeToElement, Transform } from 'react-html-parser'
|
||||
|
@ -27,6 +29,7 @@ import { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
|
|||
import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
|
||||
import { getGistReplacement } from './replace-components/gist/gist-frame'
|
||||
import { getPDFReplacement } from './replace-components/pdf/pdf-frame'
|
||||
import { getTOCReplacement } from './replace-components/toc/toc-replacer'
|
||||
import { getVimeoReplacement } from './replace-components/vimeo/vimeo-frame'
|
||||
import { getYouTubeReplacement } from './replace-components/youtube/youtube-frame'
|
||||
|
||||
|
@ -35,15 +38,16 @@ export interface MarkdownPreviewProps {
|
|||
}
|
||||
|
||||
export type SubNodeConverter = (node: DomElement, index: number) => ReactElement
|
||||
export type ComponentReplacer = (node: DomElement, counterMap: Map<string, number>, nodeConverter: SubNodeConverter) => (ReactElement | undefined);
|
||||
const allComponentReplacers: ComponentReplacer[] = [getYouTubeReplacement, getVimeoReplacement, getGistReplacement, getPDFReplacement]
|
||||
export type ComponentReplacer = (node: DomElement, index: number, counterMap: Map<string, number>, nodeConverter: SubNodeConverter) => (ReactElement | undefined);
|
||||
type ComponentReplacer2Identifier2CounterMap = Map<ComponentReplacer, Map<string, number>>
|
||||
|
||||
const tryToReplaceNode = (node: DomElement, componentReplacer2Identifier2CounterMap: ComponentReplacer2Identifier2CounterMap, nodeConverter: SubNodeConverter) => {
|
||||
const allComponentReplacers: ComponentReplacer[] = [getYouTubeReplacement, getVimeoReplacement, getGistReplacement, getPDFReplacement, getTOCReplacement]
|
||||
|
||||
const tryToReplaceNode = (node: DomElement, index:number, componentReplacer2Identifier2CounterMap: ComponentReplacer2Identifier2CounterMap, nodeConverter: SubNodeConverter) => {
|
||||
return allComponentReplacers
|
||||
.map((componentReplacer) => {
|
||||
const identifier2CounterMap = componentReplacer2Identifier2CounterMap.get(componentReplacer) || new Map<string, number>()
|
||||
return componentReplacer(node, identifier2CounterMap, nodeConverter)
|
||||
return componentReplacer(node, index, identifier2CounterMap, nodeConverter)
|
||||
})
|
||||
.find((replacement) => !!replacement)
|
||||
}
|
||||
|
@ -65,6 +69,15 @@ const MarkdownRenderer: React.FC<MarkdownPreviewProps> = ({ content }) => {
|
|||
md.use(inserted)
|
||||
md.use(marked)
|
||||
md.use(footnote)
|
||||
md.use(anchor, {
|
||||
permalink: true,
|
||||
permalinkBefore: true,
|
||||
permalinkClass: 'heading-anchor text-dark',
|
||||
permalinkSymbol: '<i class="fa fa-link"></i>'
|
||||
})
|
||||
md.use(toc, {
|
||||
markerPattern: /^\[TOC]$/i
|
||||
})
|
||||
md.use(markdownItRegex, replaceLegacyYoutubeShortCode)
|
||||
md.use(markdownItRegex, replaceLegacyVimeoShortCode)
|
||||
md.use(markdownItRegex, replaceLegacyGistShortCode)
|
||||
|
@ -87,7 +100,7 @@ const MarkdownRenderer: React.FC<MarkdownPreviewProps> = ({ content }) => {
|
|||
const componentReplacer2Identifier2CounterMap = new Map<ComponentReplacer, Map<string, number>>()
|
||||
const html: string = markdownIt.render(content)
|
||||
const transform: Transform = (node, index) => {
|
||||
const maybeReplacement = tryToReplaceNode(node, componentReplacer2Identifier2CounterMap,
|
||||
const maybeReplacement = tryToReplaceNode(node, index, componentReplacer2Identifier2CounterMap,
|
||||
(subNode, subIndex) => convertNodeToElement(subNode, subIndex, transform))
|
||||
return maybeReplacement || convertNodeToElement(node, index, transform)
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ interface resizeEvent {
|
|||
id: string
|
||||
}
|
||||
|
||||
const getElementReplacement:ComponentReplacer = (node, counterMap) => {
|
||||
const getElementReplacement:ComponentReplacer = (node, index:number, counterMap) => {
|
||||
const attributes = getAttributesFromCodiMdTag(node, 'gist')
|
||||
if (attributes && attributes.id) {
|
||||
const gistId = attributes.id
|
||||
|
|
|
@ -5,7 +5,7 @@ import { getAttributesFromCodiMdTag } from '../codi-md-tag-utils'
|
|||
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
|
||||
import './pdf-frame.scss'
|
||||
|
||||
const getElementReplacement = (node: DomElement, counterMap: Map<string, number>): (ReactElement | undefined) => {
|
||||
const getElementReplacement = (node: DomElement, index:number, counterMap: Map<string, number>): (ReactElement | undefined) => {
|
||||
const attributes = getAttributesFromCodiMdTag(node, 'pdf')
|
||||
if (attributes && attributes.url) {
|
||||
const pdfUrl = attributes.url
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { DomElement } from 'domhandler'
|
||||
import { ReactElement } from 'react'
|
||||
import { SubNodeConverter } from '../../markdown-renderer'
|
||||
|
||||
const getElementReplacement = (node: DomElement, index: number, counterMap: Map<string, number>, nodeConverter: SubNodeConverter): (ReactElement | undefined) => {
|
||||
if (node.name === 'p' && node.children && node.children.length === 1) {
|
||||
const possibleTocDiv = node.children[0]
|
||||
if (possibleTocDiv.name === 'div' && possibleTocDiv.attribs && possibleTocDiv.attribs.class &&
|
||||
possibleTocDiv.attribs.class === 'table-of-contents' && possibleTocDiv.children && possibleTocDiv.children.length === 1) {
|
||||
const listElement = possibleTocDiv.children[0]
|
||||
listElement.attribs = Object.assign(listElement.attribs || {}, { class: 'table-of-contents' })
|
||||
return nodeConverter(listElement, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { getElementReplacement as getTOCReplacement }
|
|
@ -3,7 +3,7 @@ import { ComponentReplacer } from '../../markdown-renderer'
|
|||
import { getAttributesFromCodiMdTag } from '../codi-md-tag-utils'
|
||||
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
|
||||
|
||||
const getElementReplacement:ComponentReplacer = (node, counterMap) => {
|
||||
const getElementReplacement:ComponentReplacer = (node, index:number, counterMap) => {
|
||||
const attributes = getAttributesFromCodiMdTag(node, 'vimeo')
|
||||
if (attributes && attributes.id) {
|
||||
const videoId = attributes.id
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ComponentReplacer } from '../../markdown-renderer'
|
|||
import { getAttributesFromCodiMdTag } from '../codi-md-tag-utils'
|
||||
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
|
||||
|
||||
const getElementReplacement: ComponentReplacer = (node, counterMap) => {
|
||||
const getElementReplacement: ComponentReplacer = (node, index:number, counterMap) => {
|
||||
const attributes = getAttributesFromCodiMdTag(node, 'youtube')
|
||||
if (attributes && attributes.id) {
|
||||
const videoId = attributes.id
|
||||
|
|
6
src/external-types/markdown-it-table-of-contents/index.d.ts
vendored
Normal file
6
src/external-types/markdown-it-table-of-contents/index.d.ts
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
declare module 'markdown-it-table-of-contents' {
|
||||
import MarkdownIt from 'markdown-it/lib'
|
||||
import { TOCOptions } from './interface'
|
||||
const markdownItTableOfContents: MarkdownIt.PluginWithOptions<TOCOptions>
|
||||
export = markdownItTableOfContents
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
export interface TOCOptions {
|
||||
includeLevel: number[]
|
||||
containerClass: string
|
||||
slugify: (s: string) => string
|
||||
markerPattern: RegExp
|
||||
listType: 'ul' | 'ol'
|
||||
format: (headingAsString: string) => string
|
||||
forceFullToc: boolean
|
||||
containerHeaderHtml: string
|
||||
containerFooterHtml: string
|
||||
transformLink: (link: string) => string
|
||||
}
|
17
yarn.lock
17
yarn.lock
|
@ -1587,6 +1587,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.1.0.tgz#ea3dd64c4805597311790b61e872cbd1ed2cd806"
|
||||
integrity sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==
|
||||
|
||||
"@types/markdown-it-anchor@4.0.4":
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/markdown-it-anchor/-/markdown-it-anchor-4.0.4.tgz#f36b67608d238d15024fb6508efd7ad3990209f6"
|
||||
integrity sha512-3CFeYzh8q7DaQboo1g+nnyXjPIvvtzoM+iBQU25sfcWVnjXckAvZIwAmPSVwHxnIblcTVcGq1XjEJjiBZFKkRA==
|
||||
dependencies:
|
||||
"@types/markdown-it" "*"
|
||||
|
||||
"@types/markdown-it-container@2.0.3":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/markdown-it-container/-/markdown-it-container-2.0.3.tgz#436de4c019d7d71b60f759037fd4d03611569eb8"
|
||||
|
@ -7282,6 +7289,11 @@ markdown-it-abbr@1.0.4:
|
|||
resolved "https://registry.yarnpkg.com/markdown-it-abbr/-/markdown-it-abbr-1.0.4.tgz#d66b5364521cbb3dd8aa59dadfba2fb6865c8fd8"
|
||||
integrity sha1-1mtTZFIcuz3Yqlna37ovtoZcj9g=
|
||||
|
||||
markdown-it-anchor@5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-5.3.0.tgz#d549acd64856a8ecd1bea58365ef385effbac744"
|
||||
integrity sha512-/V1MnLL/rgJ3jkMWo84UR+K+jF1cxNG1a+KwqeXqTIJ+jtA8aWSHuigx8lTzauiIjBDbwF3NcWQMotd0Dm39jA==
|
||||
|
||||
markdown-it-container@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/markdown-it-container/-/markdown-it-container-3.0.0.tgz#1d19b06040a020f9a827577bb7dbf67aa5de9a5b"
|
||||
|
@ -7327,6 +7339,11 @@ markdown-it-sup@1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz#cb9c9ff91a5255ac08f3fd3d63286e15df0a1fc3"
|
||||
integrity sha1-y5yf+RpSVawI8/09YyhuFd8KH8M=
|
||||
|
||||
markdown-it-table-of-contents@0.4.4:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.yarnpkg.com/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.4.tgz#3dc7ce8b8fc17e5981c77cc398d1782319f37fbc"
|
||||
integrity sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw==
|
||||
|
||||
markdown-it-task-lists@2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz#f68f4d2ac2bad5a2c373ba93081a1a6848417088"
|
||||
|
|
Loading…
Reference in a new issue