mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-15 07:34:42 -04:00
Add Emoji/FA Autocompletion (#387)
added emoji/fork-awesome autocompletion added autocompletion e2e test Co-authored-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Co-authored-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
c8c5569426
commit
c15f0d9900
13 changed files with 279 additions and 38 deletions
|
@ -35,6 +35,7 @@
|
||||||
- Images will be loaded via proxy if an image proxy is configured in the backend
|
- Images will be loaded via proxy if an image proxy is configured in the backend
|
||||||
- Asciinema videos may now be embedded by pasting the URL of one video into a single line
|
- Asciinema videos may now be embedded by pasting the URL of one video into a single line
|
||||||
- The Toolbar includes an EmojiPicker
|
- The Toolbar includes an EmojiPicker
|
||||||
|
- Added shortcodes for [fork-awesome icons](https://forkaweso.me/Fork-Awesome/icons/) (e.g. `:fa-picture-o:`)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
64
cypress/integration/autocompletion.spec.ts
Normal file
64
cypress/integration/autocompletion.spec.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
describe('Autocompletion', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/n/test')
|
||||||
|
cy.get('.btn.active.btn-outline-secondary > i.fa-columns')
|
||||||
|
.should('exist')
|
||||||
|
cy.get('.CodeMirror textarea')
|
||||||
|
.type('{ctrl}a', { force: true })
|
||||||
|
.type('{backspace}')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('normal emoji', () => {
|
||||||
|
it('via Enter', () => {
|
||||||
|
cy.get('.CodeMirror textarea')
|
||||||
|
.type(':book')
|
||||||
|
.type('{enter}')
|
||||||
|
cy.get('.CodeMirror-hints')
|
||||||
|
.should('not.exist')
|
||||||
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
|
.should('have.text', ':book:')
|
||||||
|
cy.get('.markdown-body')
|
||||||
|
.should('have.text', '📖')
|
||||||
|
})
|
||||||
|
it('via doubleclick', () => {
|
||||||
|
cy.get('.CodeMirror textarea')
|
||||||
|
.type(':book')
|
||||||
|
cy.get('.CodeMirror-hints > li')
|
||||||
|
.first()
|
||||||
|
.dblclick()
|
||||||
|
cy.get('.CodeMirror-hints')
|
||||||
|
.should('not.exist')
|
||||||
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
|
.should('have.text', ':book:')
|
||||||
|
cy.get('.markdown-body')
|
||||||
|
.should('have.text', '📖')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fork-awesome-icon', () => {
|
||||||
|
it('via Enter', () => {
|
||||||
|
cy.get('.CodeMirror textarea')
|
||||||
|
.type(':facebook')
|
||||||
|
.type('{enter}')
|
||||||
|
cy.get('.CodeMirror-hints')
|
||||||
|
.should('not.exist')
|
||||||
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
|
.should('have.text', ':fa-facebook:')
|
||||||
|
cy.get('.markdown-body > p > i.fa.fa-facebook')
|
||||||
|
.should('exist')
|
||||||
|
})
|
||||||
|
it('via doubleclick', () => {
|
||||||
|
cy.get('.CodeMirror textarea')
|
||||||
|
.type(':facebook')
|
||||||
|
cy.get('.CodeMirror-hints > li')
|
||||||
|
.first()
|
||||||
|
.dblclick()
|
||||||
|
cy.get('.CodeMirror-hints')
|
||||||
|
.should('not.exist')
|
||||||
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
|
.should('have.text', ':fa-facebook:')
|
||||||
|
cy.get('.markdown-body > p > i.fa.fa-facebook')
|
||||||
|
.should('exist')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,6 +1,7 @@
|
||||||
@import '../../../../node_modules/codemirror/lib/codemirror.css';
|
@import '../../../../node_modules/codemirror/lib/codemirror.css';
|
||||||
@import '../../../../node_modules/codemirror/addon/display/fullscreen.css';
|
@import '../../../../node_modules/codemirror/addon/display/fullscreen.css';
|
||||||
@import './one-dark.css';
|
@import './one-dark.css';
|
||||||
|
@import 'hints';
|
||||||
|
|
||||||
.CodeMirror {
|
.CodeMirror {
|
||||||
font-family: "Source Code Pro", "twemoji", Consolas, monaco, monospace;
|
font-family: "Source Code Pro", "twemoji", Consolas, monaco, monospace;
|
||||||
|
@ -9,3 +10,4 @@
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Editor } from 'codemirror'
|
import { Editor, EditorChange } from 'codemirror'
|
||||||
import 'codemirror/addon/comment/comment'
|
import 'codemirror/addon/comment/comment'
|
||||||
import 'codemirror/addon/display/autorefresh'
|
import 'codemirror/addon/display/autorefresh'
|
||||||
import 'codemirror/addon/display/fullscreen'
|
import 'codemirror/addon/display/fullscreen'
|
||||||
|
@ -10,14 +10,16 @@ import 'codemirror/addon/edit/matchbrackets'
|
||||||
import 'codemirror/addon/edit/matchtags'
|
import 'codemirror/addon/edit/matchtags'
|
||||||
import 'codemirror/addon/fold/foldcode'
|
import 'codemirror/addon/fold/foldcode'
|
||||||
import 'codemirror/addon/fold/foldgutter'
|
import 'codemirror/addon/fold/foldgutter'
|
||||||
|
import 'codemirror/addon/hint/show-hint'
|
||||||
import 'codemirror/addon/search/match-highlighter'
|
import 'codemirror/addon/search/match-highlighter'
|
||||||
import 'codemirror/addon/selection/active-line'
|
import 'codemirror/addon/selection/active-line'
|
||||||
import 'codemirror/keymap/sublime.js'
|
import 'codemirror/keymap/sublime.js'
|
||||||
import 'codemirror/mode/gfm/gfm.js'
|
import 'codemirror/mode/gfm/gfm.js'
|
||||||
import React, { useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
|
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import './editor-window.scss'
|
import './editor-window.scss'
|
||||||
|
import { emojiHints, emojiWordRegex, findWordAtCursor } from './hints/emoji'
|
||||||
import { defaultKeyMap } from './key-map'
|
import { defaultKeyMap } from './key-map'
|
||||||
import { ToolBar } from './tool-bar/tool-bar'
|
import { ToolBar } from './tool-bar/tool-bar'
|
||||||
|
|
||||||
|
@ -26,10 +28,28 @@ export interface EditorWindowProps {
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hintOptions = {
|
||||||
|
hint: emojiHints,
|
||||||
|
completeSingle: false,
|
||||||
|
completeOnSingleClick: false,
|
||||||
|
alignWithWord: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = (editor: Editor) => {
|
||||||
|
const searchTerm = findWordAtCursor(editor)
|
||||||
|
if (emojiWordRegex.test(searchTerm.text)) {
|
||||||
|
editor.showHint(hintOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, content }) => {
|
export const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, content }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [editor, setEditor] = useState<Editor>()
|
const [editor, setEditor] = useState<Editor>()
|
||||||
|
|
||||||
|
const onBeforeChange = useCallback((editor: Editor, data: EditorChange, value: string) => {
|
||||||
|
onContentChange(value)
|
||||||
|
}, [onContentChange])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'d-flex flex-column h-100'}>
|
<div className={'d-flex flex-column h-100'}>
|
||||||
<ToolBar
|
<ToolBar
|
||||||
|
@ -67,12 +87,13 @@ export const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, con
|
||||||
addModeClass: true,
|
addModeClass: true,
|
||||||
autoRefresh: true,
|
autoRefresh: true,
|
||||||
// otherCursors: true,
|
// otherCursors: true,
|
||||||
placeholder: t('editor.placeholder')
|
placeholder: t('editor.placeholder'),
|
||||||
|
showHint: false,
|
||||||
|
hintOptions: hintOptions
|
||||||
}}
|
}}
|
||||||
editorDidMount={mountedEditor => setEditor(mountedEditor)}
|
editorDidMount={mountedEditor => setEditor(mountedEditor)}
|
||||||
onBeforeChange={(editor, data, value) => {
|
onBeforeChange={onBeforeChange}
|
||||||
onContentChange(value)
|
onChange={onChange}
|
||||||
}}
|
|
||||||
/></div>
|
/></div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
32
src/components/editor/editor-window/hints.scss
Normal file
32
src/components/editor/editor-window/hints.scss
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
.CodeMirror-hints {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
overflow: hidden;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
box-shadow: 2px 3px 5px rgba(0,0,0,.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid silver;
|
||||||
|
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
max-height: 20em;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-hint {
|
||||||
|
margin: 0;
|
||||||
|
padding: 3px 15px;
|
||||||
|
border-radius: 2px;
|
||||||
|
white-space: pre;
|
||||||
|
color: black;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.CodeMirror-hint-active {
|
||||||
|
background: #08f;
|
||||||
|
color: white;
|
||||||
|
}
|
75
src/components/editor/editor-window/hints/emoji.ts
Normal file
75
src/components/editor/editor-window/hints/emoji.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
||||||
|
import { Data, EmojiData, NimbleEmojiIndex } from 'emoji-mart'
|
||||||
|
import data from 'emoji-mart/data/twitter.json'
|
||||||
|
import { getEmojiIcon, getEmojiShortCode } from '../../../../utils/emoji'
|
||||||
|
import { customEmojis } from '../tool-bar/emoji-picker/emoji-picker'
|
||||||
|
|
||||||
|
interface findWordAtCursorResponse {
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedCharsInEmojiCodeRegex = /(:|\w|-|_|\+)/
|
||||||
|
const emojiIndex = new NimbleEmojiIndex(data as unknown as Data)
|
||||||
|
|
||||||
|
export const emojiWordRegex = /^:((\w|-|_|\+)+)$/
|
||||||
|
|
||||||
|
export const findWordAtCursor = (editor: Editor): findWordAtCursorResponse => {
|
||||||
|
const cursor = editor.getCursor()
|
||||||
|
const line = editor.getLine(cursor.line)
|
||||||
|
let start = cursor.ch
|
||||||
|
let end = cursor.ch
|
||||||
|
while (start && allowedCharsInEmojiCodeRegex.test(line.charAt(start - 1))) {
|
||||||
|
--start
|
||||||
|
}
|
||||||
|
while (end < line.length && allowedCharsInEmojiCodeRegex.test(line.charAt(end))) {
|
||||||
|
++end
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: line.slice(start, end).toLowerCase(),
|
||||||
|
start: start,
|
||||||
|
end: end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const emojiHints = (editor: Editor): Promise< Hints| null > => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const searchTerm = findWordAtCursor(editor)
|
||||||
|
const searchResult = emojiWordRegex.exec(searchTerm.text)
|
||||||
|
if (searchResult === null) {
|
||||||
|
resolve(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const term = searchResult[1]
|
||||||
|
if (!term) {
|
||||||
|
resolve(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const search = emojiIndex.search(term, {
|
||||||
|
emojisToShowFilter: () => true,
|
||||||
|
maxResults: 5,
|
||||||
|
include: [],
|
||||||
|
exclude: [],
|
||||||
|
custom: customEmojis as EmojiData[]
|
||||||
|
})
|
||||||
|
const cursor = editor.getCursor()
|
||||||
|
if (!search) {
|
||||||
|
resolve(null)
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
list: search.map((emojiData: EmojiData): Hint => ({
|
||||||
|
text: getEmojiShortCode(emojiData),
|
||||||
|
render: (parent: HTMLLIElement) => {
|
||||||
|
const wrapper = document.createElement('div')
|
||||||
|
wrapper.innerHTML = `${getEmojiIcon(emojiData)} ${getEmojiShortCode(emojiData)}`
|
||||||
|
parent.appendChild(wrapper)
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
from: Pos(cursor.line, searchTerm.start),
|
||||||
|
to: Pos(cursor.line, searchTerm.end)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { Data, EmojiData, NimblePicker } from 'emoji-mart'
|
import { CustomEmoji, Data, EmojiData, NimblePicker } from 'emoji-mart'
|
||||||
import 'emoji-mart/css/emoji-mart.css'
|
import 'emoji-mart/css/emoji-mart.css'
|
||||||
import emojiData from 'emoji-mart/data/twitter.json'
|
import emojiData from 'emoji-mart/data/twitter.json'
|
||||||
import React, { useMemo, useRef } from 'react'
|
import React, { useRef } from 'react'
|
||||||
import { useClickAway } from 'react-use'
|
import { useClickAway } from 'react-use'
|
||||||
import { ShowIf } from '../../../../common/show-if/show-if'
|
import { ShowIf } from '../../../../common/show-if/show-if'
|
||||||
import './emoji-picker.scss'
|
import './emoji-picker.scss'
|
||||||
|
@ -13,18 +13,18 @@ export interface EmojiPickerProps {
|
||||||
onDismiss: () => void
|
onDismiss: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const customEmojis: CustomEmoji[] = Object.keys(ForkAwesomeIcons).map((name) => ({
|
||||||
|
name: `fa-${name}`,
|
||||||
|
short_names: [`fa-${name.toLowerCase()}`],
|
||||||
|
text: '',
|
||||||
|
emoticons: [],
|
||||||
|
keywords: ['fork awesome'],
|
||||||
|
imageUrl: '/img/forkawesome.png',
|
||||||
|
customCategory: 'ForkAwesome'
|
||||||
|
}))
|
||||||
|
|
||||||
export const EmojiPicker: React.FC<EmojiPickerProps> = ({ show, onEmojiSelected, onDismiss }) => {
|
export const EmojiPicker: React.FC<EmojiPickerProps> = ({ show, onEmojiSelected, onDismiss }) => {
|
||||||
const pickerRef = useRef(null)
|
const pickerRef = useRef(null)
|
||||||
const customIcons = useMemo(() =>
|
|
||||||
Object.keys(ForkAwesomeIcons).map((name) => ({
|
|
||||||
name: `fa-${name}`,
|
|
||||||
short_names: [`fa-${name.toLowerCase()}`],
|
|
||||||
text: '',
|
|
||||||
emoticons: [],
|
|
||||||
keywords: ['fork awesome'],
|
|
||||||
imageUrl: '/img/forkawesome.png',
|
|
||||||
customCategory: 'ForkAwesome'
|
|
||||||
})), [])
|
|
||||||
|
|
||||||
useClickAway(pickerRef, () => {
|
useClickAway(pickerRef, () => {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
|
@ -39,7 +39,7 @@ export const EmojiPicker: React.FC<EmojiPickerProps> = ({ show, onEmojiSelected,
|
||||||
onSelect={onEmojiSelected}
|
onSelect={onEmojiSelected}
|
||||||
theme={'auto'}
|
theme={'auto'}
|
||||||
title=''
|
title=''
|
||||||
custom={customIcons}
|
custom={customEmojis}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
|
|
|
@ -1738,10 +1738,10 @@ describe('test addEmoji with native emoji', () => {
|
||||||
describe('test addEmoji with native emoji', () => {
|
describe('test addEmoji with native emoji', () => {
|
||||||
const { cursor, firstLine, multiline, multilineOffset } = buildRanges()
|
const { cursor, firstLine, multiline, multilineOffset } = buildRanges()
|
||||||
const textFirstLine = testContent.split('\n')[0]
|
const textFirstLine = testContent.split('\n')[0]
|
||||||
// noinspection CheckTagEmptyBody
|
const forkAwesomeIcon = ':fa-star:'
|
||||||
const forkAwesomeIcon = '<i class="fa star"></i>'
|
|
||||||
const emoji = Mock.of<EmojiData>({
|
const emoji = Mock.of<EmojiData>({
|
||||||
name: 'star',
|
name: 'star',
|
||||||
|
colons: ':fa-star:',
|
||||||
imageUrl: '/img/forkawesome.png'
|
imageUrl: '/img/forkawesome.png'
|
||||||
})
|
})
|
||||||
it('just cursor', done => {
|
it('just cursor', done => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Editor } from 'codemirror'
|
import { Editor } from 'codemirror'
|
||||||
import { BaseEmoji, CustomEmoji, EmojiData } from 'emoji-mart'
|
import { EmojiData } from 'emoji-mart'
|
||||||
|
import { getEmojiShortCode } from '../../../../utils/emoji'
|
||||||
|
|
||||||
export const makeSelectionBold = (editor: Editor): void => wrapTextWith(editor, '**')
|
export const makeSelectionBold = (editor: Editor): void => wrapTextWith(editor, '**')
|
||||||
export const makeSelectionItalic = (editor: Editor): void => wrapTextWith(editor, '*')
|
export const makeSelectionItalic = (editor: Editor): void => wrapTextWith(editor, '*')
|
||||||
|
@ -24,14 +25,7 @@ export const addComment = (editor: Editor): void => changeLines(editor, line =>
|
||||||
export const addTable = (editor: Editor): void => changeLines(editor, line => `${line}\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |`)
|
export const addTable = (editor: Editor): void => changeLines(editor, line => `${line}\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |`)
|
||||||
|
|
||||||
export const addEmoji = (emoji: EmojiData, editor: Editor): void => {
|
export const addEmoji = (emoji: EmojiData, editor: Editor): void => {
|
||||||
let replacement = ''
|
insertAtCursor(editor, getEmojiShortCode(emoji))
|
||||||
if ((emoji as BaseEmoji).colons) {
|
|
||||||
replacement = (emoji as BaseEmoji).colons
|
|
||||||
} else if ((emoji as CustomEmoji).imageUrl) {
|
|
||||||
// noinspection CheckTagEmptyBody
|
|
||||||
replacement = `<i class="fa ${(emoji as CustomEmoji).name}"></i>`
|
|
||||||
}
|
|
||||||
insertAtCursor(editor, replacement)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const wrapTextWith = (editor: Editor, symbol: string, endSymbol?: string): void => {
|
export const wrapTextWith = (editor: Editor, symbol: string, endSymbol?: string): void => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import equal from 'deep-equal'
|
import equal from 'deep-equal'
|
||||||
import { DomElement } from 'domhandler'
|
import { DomElement } from 'domhandler'
|
||||||
|
import emojiData from 'emoji-mart/data/twitter.json'
|
||||||
import { Data } from 'emoji-mart/dist-es/utils/data'
|
import { Data } from 'emoji-mart/dist-es/utils/data'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
|
@ -14,8 +15,8 @@ import imsize from 'markdown-it-imsize'
|
||||||
import inserted from 'markdown-it-ins'
|
import inserted from 'markdown-it-ins'
|
||||||
import marked from 'markdown-it-mark'
|
import marked from 'markdown-it-mark'
|
||||||
import mathJax from 'markdown-it-mathjax'
|
import mathJax from 'markdown-it-mathjax'
|
||||||
import markdownItRegex from 'markdown-it-regex'
|
|
||||||
import plantuml from 'markdown-it-plantuml'
|
import plantuml from 'markdown-it-plantuml'
|
||||||
|
import markdownItRegex from 'markdown-it-regex'
|
||||||
import subscript from 'markdown-it-sub'
|
import subscript from 'markdown-it-sub'
|
||||||
import superscript from 'markdown-it-sup'
|
import superscript from 'markdown-it-sup'
|
||||||
import taskList from 'markdown-it-task-lists'
|
import taskList from 'markdown-it-task-lists'
|
||||||
|
@ -31,13 +32,14 @@ import { ApplicationState } from '../../../redux'
|
||||||
import { slugify } from '../../../utils/slugify'
|
import { slugify } from '../../../utils/slugify'
|
||||||
import { InternalLink } from '../../common/links/internal-link'
|
import { InternalLink } from '../../common/links/internal-link'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
|
import { ForkAwesomeIcons } from '../editor-window/tool-bar/emoji-picker/icon-names'
|
||||||
import { RawYAMLMetadata, YAMLMetaData } from '../yaml-metadata/yaml-metadata'
|
import { RawYAMLMetadata, YAMLMetaData } from '../yaml-metadata/yaml-metadata'
|
||||||
import { createRenderContainer, validAlertLevels } from './container-plugins/alert'
|
import { createRenderContainer, validAlertLevels } from './container-plugins/alert'
|
||||||
import { highlightedCode } from './markdown-it-plugins/highlighted-code'
|
import { highlightedCode } from './markdown-it-plugins/highlighted-code'
|
||||||
import { linkifyExtra } from './markdown-it-plugins/linkify-extra'
|
import { linkifyExtra } from './markdown-it-plugins/linkify-extra'
|
||||||
import { MarkdownItParserDebugger } from './markdown-it-plugins/parser-debugger'
|
import { MarkdownItParserDebugger } from './markdown-it-plugins/parser-debugger'
|
||||||
import './markdown-renderer.scss'
|
|
||||||
import { plantumlError } from './markdown-it-plugins/plantuml-error'
|
import { plantumlError } from './markdown-it-plugins/plantuml-error'
|
||||||
|
import './markdown-renderer.scss'
|
||||||
import { replaceAsciinemaLink } from './regex-plugins/replace-asciinema-link'
|
import { replaceAsciinemaLink } from './regex-plugins/replace-asciinema-link'
|
||||||
import { replaceGistLink } from './regex-plugins/replace-gist-link'
|
import { replaceGistLink } from './regex-plugins/replace-gist-link'
|
||||||
import { replaceLegacyGistShortCode } from './regex-plugins/replace-legacy-gist-short-code'
|
import { replaceLegacyGistShortCode } from './regex-plugins/replace-legacy-gist-short-code'
|
||||||
|
@ -51,8 +53,8 @@ import { replaceQuoteExtraColor } from './regex-plugins/replace-quote-extra-colo
|
||||||
import { replaceQuoteExtraTime } from './regex-plugins/replace-quote-extra-time'
|
import { replaceQuoteExtraTime } from './regex-plugins/replace-quote-extra-time'
|
||||||
import { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
|
import { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
|
||||||
import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
|
import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
|
||||||
import { ComponentReplacer, SubNodeConverter } from './replace-components/ComponentReplacer'
|
|
||||||
import { AsciinemaReplacer } from './replace-components/asciinema/asciinema-replacer'
|
import { AsciinemaReplacer } from './replace-components/asciinema/asciinema-replacer'
|
||||||
|
import { ComponentReplacer, SubNodeConverter } from './replace-components/ComponentReplacer'
|
||||||
import { GistReplacer } from './replace-components/gist/gist-replacer'
|
import { GistReplacer } from './replace-components/gist/gist-replacer'
|
||||||
import { HighlightedCodeReplacer } from './replace-components/highlighted-fence/highlighted-fence-replacer'
|
import { HighlightedCodeReplacer } from './replace-components/highlighted-fence/highlighted-fence-replacer'
|
||||||
import { ImageReplacer } from './replace-components/image/image-replacer'
|
import { ImageReplacer } from './replace-components/image/image-replacer'
|
||||||
|
@ -63,7 +65,6 @@ import { QuoteOptionsReplacer } from './replace-components/quote-options/quote-o
|
||||||
import { TocReplacer } from './replace-components/toc/toc-replacer'
|
import { TocReplacer } from './replace-components/toc/toc-replacer'
|
||||||
import { VimeoReplacer } from './replace-components/vimeo/vimeo-replacer'
|
import { VimeoReplacer } from './replace-components/vimeo/vimeo-replacer'
|
||||||
import { YoutubeReplacer } from './replace-components/youtube/youtube-replacer'
|
import { YoutubeReplacer } from './replace-components/youtube/youtube-replacer'
|
||||||
import emojiData from 'emoji-mart/data/twitter.json'
|
|
||||||
|
|
||||||
export interface MarkdownRendererProps {
|
export interface MarkdownRendererProps {
|
||||||
content: string
|
content: string
|
||||||
|
@ -77,12 +78,29 @@ export interface MarkdownRendererProps {
|
||||||
const markdownItTwitterEmojis = Object.keys((emojiData as unknown as Data).emojis)
|
const markdownItTwitterEmojis = Object.keys((emojiData as unknown as Data).emojis)
|
||||||
.reduce((reduceObject, emojiIdentifier) => {
|
.reduce((reduceObject, emojiIdentifier) => {
|
||||||
const emoji = (emojiData as unknown as Data).emojis[emojiIdentifier]
|
const emoji = (emojiData as unknown as Data).emojis[emojiIdentifier]
|
||||||
if (emoji.b) {
|
if (emoji.unified) {
|
||||||
reduceObject[emojiIdentifier] = `&#x${emoji.b};`
|
reduceObject[emojiIdentifier] = emoji.unified.split('-').map(char => `&#x${char};`).join('')
|
||||||
}
|
}
|
||||||
return reduceObject
|
return reduceObject
|
||||||
}, {} as { [key: string]: string })
|
}, {} as { [key: string]: string })
|
||||||
|
|
||||||
|
const emojiSkinToneModifierMap = [2, 3, 4, 5, 6]
|
||||||
|
.reduce((reduceObject, modifierValue) => {
|
||||||
|
const lightSkinCode = 127995
|
||||||
|
const codepoint = lightSkinCode + (modifierValue - 2)
|
||||||
|
const shortcode = `skin-tone-${modifierValue}`
|
||||||
|
reduceObject[shortcode] = `&#${codepoint};`
|
||||||
|
return reduceObject
|
||||||
|
}, {} as { [key: string]: string })
|
||||||
|
|
||||||
|
const forkAwesomeIconMap = Object.keys(ForkAwesomeIcons)
|
||||||
|
.reduce((reduceObject, icon) => {
|
||||||
|
const shortcode = `fa-${icon}`
|
||||||
|
// noinspection CheckTagEmptyBody
|
||||||
|
reduceObject[shortcode] = `<i class="fa fa-${icon}"></i>`
|
||||||
|
return reduceObject
|
||||||
|
}, {} as { [key: string]: string })
|
||||||
|
|
||||||
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onMetaDataChange, onFirstHeadingChange, onTocChange, className, wide }) => {
|
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onMetaDataChange, onFirstHeadingChange, onTocChange, className, wide }) => {
|
||||||
const [tocAst, setTocAst] = useState<TocAst>()
|
const [tocAst, setTocAst] = useState<TocAst>()
|
||||||
const [lastTocAst, setLastTocAst] = useState<TocAst>()
|
const [lastTocAst, setLastTocAst] = useState<TocAst>()
|
||||||
|
@ -154,7 +172,11 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onM
|
||||||
md.use(plantumlError)
|
md.use(plantumlError)
|
||||||
}
|
}
|
||||||
md.use(emoji, {
|
md.use(emoji, {
|
||||||
defs: markdownItTwitterEmojis
|
defs: {
|
||||||
|
...markdownItTwitterEmojis,
|
||||||
|
...emojiSkinToneModifierMap,
|
||||||
|
...forkAwesomeIconMap
|
||||||
|
}
|
||||||
})
|
})
|
||||||
md.use(abbreviation)
|
md.use(abbreviation)
|
||||||
md.use(definitionList)
|
md.use(definitionList)
|
||||||
|
|
15
src/external-types/emoji-mart/dist-es/utils/emoji-index/nimble-emoji-index.d.ts
vendored
Normal file
15
src/external-types/emoji-mart/dist-es/utils/emoji-index/nimble-emoji-index.d.ts
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import 'emoji-mart'
|
||||||
|
|
||||||
|
declare module 'emoji-mart' {
|
||||||
|
export interface SearchOption {
|
||||||
|
emojisToShowFilter: (emoji: EmojiData) => boolean
|
||||||
|
maxResults: number,
|
||||||
|
include: EmojiData[]
|
||||||
|
exclude: EmojiData[]
|
||||||
|
custom: EmojiData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NimbleEmojiIndex {
|
||||||
|
search (query: string, options: SearchOption): EmojiData[] | null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ html {
|
||||||
body {
|
body {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
background-color: darken($dark, 8%);
|
background-color: darken($dark, 8%);
|
||||||
font-family: "Source Sans Pro", Helvetica, Arial, sans-serif;
|
font-family: "Source Sans Pro", Helvetica, Arial, twemoji, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
*:focus {
|
*:focus {
|
||||||
|
|
15
src/utils/emoji.ts
Normal file
15
src/utils/emoji.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { BaseEmoji, CustomEmoji, EmojiData } from 'emoji-mart'
|
||||||
|
|
||||||
|
export const getEmojiIcon = (emoji: EmojiData):string => {
|
||||||
|
if ((emoji as BaseEmoji).native) {
|
||||||
|
return (emoji as BaseEmoji).native
|
||||||
|
} else if ((emoji as CustomEmoji).imageUrl) {
|
||||||
|
// noinspection CheckTagEmptyBody
|
||||||
|
return `<i class="fa ${(emoji as CustomEmoji).name}"></i>`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getEmojiShortCode = (emoji: EmojiData):string => {
|
||||||
|
return (emoji as BaseEmoji).colons
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue