mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-16 16:14:43 -04:00
add missing autocompletions (#514)
* added missing autocompletions: - code-block - container - header - image - link - pdf * added extraTags ([name=], [time=], [color=]) to the link autocompletion, because they trigger on the same characters added getUser in /redux/user/methods to retrive the current user outside of .tsx files improve the regexps on several autocompletion * renamed hints to auto Co-authored-by: Erik Michelson <github@erik.michelson.eu> Co-authored-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
2decfc1fa2
commit
db4f2a4478
11 changed files with 599 additions and 86 deletions
|
@ -8,56 +8,260 @@ describe('Autocompletion', () => {
|
|||
.type('{backspace}')
|
||||
})
|
||||
|
||||
describe('normal emoji', () => {
|
||||
describe('code block', () => {
|
||||
it('via Enter', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type(':book')
|
||||
.type('```')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('.CodeMirror textarea')
|
||||
.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')
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(1) > .CodeMirror-line > span > span')
|
||||
.should('have.text', '```1c')
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
||||
.should('have.text', '```')
|
||||
cy.get('.markdown-body > pre > code')
|
||||
.should('exist')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type(':facebook')
|
||||
.type('```')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(1) > .CodeMirror-line > span > span')
|
||||
.should('have.text', '```1c')
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
||||
.should('have.text', '```')
|
||||
cy.get('.markdown-body > pre > code')
|
||||
.should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('container', () => {
|
||||
it('via Enter', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type(':::')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('{enter}')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(1) > .CodeMirror-line > span > span')
|
||||
.should('have.text', ':::success')
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
||||
.should('have.text', ':::')
|
||||
cy.get('.markdown-body > div.alert')
|
||||
.should('exist')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type(':::')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(1) > .CodeMirror-line > span > span')
|
||||
.should('have.text', ':::success')
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
||||
.should('have.text', ':::')
|
||||
cy.get('.markdown-body > div.alert')
|
||||
.should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('emoji', () => {
|
||||
describe('normal emoji', () => {
|
||||
it('via Enter', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type(':book')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('.CodeMirror textarea')
|
||||
.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')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('.CodeMirror textarea')
|
||||
.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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('header', () => {
|
||||
it('via Enter', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('#')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('{enter}')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '# ')
|
||||
cy.get('.markdown-body > h1 ')
|
||||
.should('have.text', ' ')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('#')
|
||||
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('have.text', '# ')
|
||||
cy.get('.markdown-body > h1')
|
||||
.should('have.text', ' ')
|
||||
})
|
||||
})
|
||||
|
||||
describe('images', () => {
|
||||
it('via Enter', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('!')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('{enter}')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '')
|
||||
cy.get('.markdown-body > p > img')
|
||||
.should('have.attr', 'alt', 'image alt')
|
||||
.should('have.attr', 'src', 'https://')
|
||||
.should('have.attr', 'title', 'title')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('!')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '')
|
||||
cy.get('.markdown-body > p > img')
|
||||
.should('have.attr', 'alt', 'image alt')
|
||||
.should('have.attr', 'src', 'https://')
|
||||
.should('have.attr', 'title', 'title')
|
||||
})
|
||||
})
|
||||
|
||||
describe('links', () => {
|
||||
it('via Enter', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('[')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('{enter}')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '[link text](https:// "title") ')
|
||||
cy.get('.markdown-body > p > a')
|
||||
.should('have.text', 'link text')
|
||||
.should('have.attr', 'href', 'https://')
|
||||
.should('have.attr', 'title', 'title')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('[')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '[link text](https:// "title") ')
|
||||
cy.get('.markdown-body > p > a')
|
||||
.should('have.text', 'link text')
|
||||
.should('have.attr', 'href', 'https://')
|
||||
.should('have.attr', 'title', 'title')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pdf', () => {
|
||||
it('via Enter', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('{')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('{enter}')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '{%pdf https:// %}')
|
||||
cy.get('.markdown-body > p')
|
||||
.should('exist')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('{')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '{%pdf https:// %}')
|
||||
cy.get('.markdown-body > p')
|
||||
.should('exist')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
||||
import hljs from 'highlight.js'
|
||||
import { findWordAtCursor, Hinter, search } from './index'
|
||||
|
||||
const allowedChars = /[`\w-_+]/
|
||||
const wordRegExp = /^```((\w|-|_|\+)*)$/
|
||||
const allSupportedLanguages = hljs.listLanguages().concat('csv', 'flow', 'html')
|
||||
|
||||
const codeBlockHint = (editor: Editor): Promise< Hints| null > => {
|
||||
return new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor, allowedChars)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const term = searchResult[1]
|
||||
const suggestions = search(term, allSupportedLanguages)
|
||||
const cursor = editor.getCursor()
|
||||
if (!suggestions) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: suggestions.map((suggestion: string): Hint => ({
|
||||
text: '```' + suggestion + '\n\n```\n',
|
||||
displayText: suggestion
|
||||
})),
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const CodeBlockHinter: Hinter = {
|
||||
allowedChars,
|
||||
wordRegExp,
|
||||
hint: codeBlockHint
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
||||
import { findWordAtCursor, Hinter } from './index'
|
||||
|
||||
const allowedChars = /[:\w-_+]/
|
||||
const wordRegExp = /^:::((\w|-|_|\+)*)$/
|
||||
const allSupportedConatiner = ['success', 'info', 'warning', 'danger']
|
||||
|
||||
const containerHint = (editor: Editor): Promise< Hints| null > => {
|
||||
return new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor, allowedChars)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const suggestions = allSupportedConatiner
|
||||
const cursor = editor.getCursor()
|
||||
if (!suggestions) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: suggestions.map((suggestion: string): Hint => ({
|
||||
text: ':::' + suggestion + '\n\n:::\n',
|
||||
displayText: suggestion
|
||||
})),
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const ContainerHinter: Hinter = {
|
||||
allowedChars,
|
||||
wordRegExp,
|
||||
hint: containerHint
|
||||
}
|
|
@ -1,64 +1,40 @@
|
|||
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 '../tool-bar/utils/emojiUtils'
|
||||
import { customEmojis } from '../tool-bar/emoji-picker/emoji-picker'
|
||||
import { getEmojiIcon, getEmojiShortCode } from '../tool-bar/utils/emojiUtils'
|
||||
import { findWordAtCursor, Hinter } from './index'
|
||||
|
||||
interface findWordAtCursorResponse {
|
||||
start: number,
|
||||
end: number,
|
||||
text: string
|
||||
}
|
||||
|
||||
const allowedCharsInEmojiCodeRegex = /(:|\w|-|_|\+)/
|
||||
const allowedCharsInEmojiCodeRegex = /[:\w-_+]/
|
||||
const emojiIndex = new NimbleEmojiIndex(data as unknown as Data)
|
||||
export const emojiWordRegex = /^:((\w|-|_|\+)+)$/
|
||||
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 generateEmojiHints = (editor: Editor): Promise< Hints| null > => {
|
||||
const generateEmojiHints = (editor: Editor): Promise< Hints| null > => {
|
||||
return new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor)
|
||||
const searchTerm = findWordAtCursor(editor, allowedCharsInEmojiCodeRegex)
|
||||
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, {
|
||||
let search: EmojiData[] | null = emojiIndex.search(term, {
|
||||
emojisToShowFilter: () => true,
|
||||
maxResults: 5,
|
||||
maxResults: 7,
|
||||
include: [],
|
||||
exclude: [],
|
||||
custom: customEmojis as EmojiData[]
|
||||
})
|
||||
if (search === null) {
|
||||
// set search to the first seven emojis in data
|
||||
search = Object.values(emojiIndex.emojis).slice(0, 7)
|
||||
}
|
||||
const cursor = editor.getCursor()
|
||||
if (!search) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: search.map((emojiData: EmojiData): Hint => ({
|
||||
list: search.map((emojiData): Hint => ({
|
||||
text: getEmojiShortCode(emojiData),
|
||||
render: (parent: HTMLLIElement) => {
|
||||
const wrapper = document.createElement('div')
|
||||
|
@ -72,3 +48,9 @@ export const generateEmojiHints = (editor: Editor): Promise< Hints| null > => {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const EmojiHinter: Hinter = {
|
||||
allowedChars: allowedCharsInEmojiCodeRegex,
|
||||
wordRegExp: emojiWordRegex,
|
||||
hint: generateEmojiHints
|
||||
}
|
43
src/components/editor/editor-pane/autocompletion/header.ts
Normal file
43
src/components/editor/editor-pane/autocompletion/header.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
||||
import { findWordAtCursor, Hinter, search } from './index'
|
||||
|
||||
const allowedChars = /#/
|
||||
const wordRegExp = /^(\s{0,3})(#{1,6})$/
|
||||
const allSupportedHeaders = ['# h1', '## h2', '### h3', '#### h4', '##### h5', '###### h6', '###### tags: `example`']
|
||||
const allSupportedHeadersTextToInsert = ['# ', '## ', '### ', '#### ', '##### ', '###### ', '###### tags: `example`']
|
||||
|
||||
const headerHint = (editor: Editor): Promise< Hints| null > => {
|
||||
return new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor, allowedChars)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const term = searchResult[0]
|
||||
if (!term) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const suggestions = search(term, allSupportedHeaders)
|
||||
const cursor = editor.getCursor()
|
||||
if (!suggestions) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: suggestions.map((suggestion, index): Hint => ({
|
||||
text: allSupportedHeadersTextToInsert[index],
|
||||
displayText: suggestion
|
||||
})),
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const HeaderHinter: Hinter = {
|
||||
allowedChars,
|
||||
wordRegExp,
|
||||
hint: headerHint
|
||||
}
|
40
src/components/editor/editor-pane/autocompletion/image.ts
Normal file
40
src/components/editor/editor-pane/autocompletion/image.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
||||
import { findWordAtCursor, Hinter } from './index'
|
||||
|
||||
const allowedChars = /[![\]\w]/
|
||||
const wordRegExp = /^(!(\[.*])?)$/
|
||||
const allSupportedImages = [
|
||||
'',
|
||||
'',
|
||||
'![image alt][reference]'
|
||||
]
|
||||
|
||||
const imageHint = (editor: Editor): Promise< Hints| null > => {
|
||||
return new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor, allowedChars)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const suggestions = allSupportedImages
|
||||
const cursor = editor.getCursor()
|
||||
if (!suggestions) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: suggestions.map((suggestion: string): Hint => ({
|
||||
text: suggestion
|
||||
})),
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end + 1)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const ImageHinter: Hinter = {
|
||||
allowedChars,
|
||||
wordRegExp,
|
||||
hint: imageHint
|
||||
}
|
59
src/components/editor/editor-pane/autocompletion/index.ts
Normal file
59
src/components/editor/editor-pane/autocompletion/index.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { Editor, Hints } from 'codemirror'
|
||||
import { CodeBlockHinter } from './code-block'
|
||||
import { ContainerHinter } from './container'
|
||||
import { EmojiHinter } from './emoji'
|
||||
import { HeaderHinter } from './header'
|
||||
import { ImageHinter } from './image'
|
||||
import { LinkAndExtraTagHinter } from './link-and-extra-tag'
|
||||
import { PDFHinter } from './pdf'
|
||||
|
||||
interface findWordAtCursorResponse {
|
||||
start: number,
|
||||
end: number,
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface Hinter {
|
||||
allowedChars: RegExp,
|
||||
wordRegExp: RegExp,
|
||||
hint: (editor: Editor) => Promise< Hints| null >
|
||||
}
|
||||
|
||||
export const findWordAtCursor = (editor: Editor, allowedChars: RegExp): findWordAtCursorResponse => {
|
||||
const cursor = editor.getCursor()
|
||||
const line = editor.getLine(cursor.line)
|
||||
let start = cursor.ch
|
||||
let end = cursor.ch
|
||||
while (start && allowedChars.test(line.charAt(start - 1))) {
|
||||
--start
|
||||
}
|
||||
while (end < line.length && allowedChars.test(line.charAt(end))) {
|
||||
++end
|
||||
}
|
||||
|
||||
return {
|
||||
text: line.slice(start, end).toLowerCase(),
|
||||
start: start,
|
||||
end: end
|
||||
}
|
||||
}
|
||||
|
||||
export const search = (term: string, list: string[]): string[] => {
|
||||
const suggestions: string[] = []
|
||||
list.forEach(item => {
|
||||
if (item.toLowerCase().startsWith(term.toLowerCase())) {
|
||||
suggestions.push(item)
|
||||
}
|
||||
})
|
||||
return suggestions.slice(0, 7)
|
||||
}
|
||||
|
||||
export const allHinters: Hinter[] = [
|
||||
CodeBlockHinter,
|
||||
ContainerHinter,
|
||||
EmojiHinter,
|
||||
HeaderHinter,
|
||||
ImageHinter,
|
||||
LinkAndExtraTagHinter,
|
||||
PDFHinter
|
||||
]
|
|
@ -0,0 +1,69 @@
|
|||
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
||||
import moment from 'moment'
|
||||
import { getUser } from '../../../../redux/user/methods'
|
||||
import { findWordAtCursor, Hinter } from './index'
|
||||
|
||||
const allowedChars = /[[\]\w]/
|
||||
const wordRegExp = /^(\[(.*])?)$/
|
||||
const allSupportedLinks = [
|
||||
'[link text](https:// "title")',
|
||||
'[reference]: https:// "title"',
|
||||
'[link text][reference]',
|
||||
'[reference]',
|
||||
'[^footnote reference]: https://',
|
||||
'[^footnote reference]',
|
||||
'^[inline footnote]',
|
||||
'[TOC]',
|
||||
'name',
|
||||
'time',
|
||||
'[color=#FFFFFF]'
|
||||
|
||||
]
|
||||
|
||||
const linkAndExtraTagHint = (editor: Editor): Promise< Hints| null > => {
|
||||
return new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor, allowedChars)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const suggestions = allSupportedLinks
|
||||
const cursor = editor.getCursor()
|
||||
if (!suggestions) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: suggestions.map((suggestion: string): Hint => {
|
||||
const user = getUser()
|
||||
const userName = user ? user.name : 'Anonymous'
|
||||
switch (suggestion) {
|
||||
case 'name':
|
||||
// Get the user when a completion happens, this prevents to early calls resulting in 'Anonymous'
|
||||
return {
|
||||
text: `[name=${userName}]`
|
||||
}
|
||||
case 'time':
|
||||
// show the current time when the autocompletion is opened and not when the function is loaded
|
||||
return {
|
||||
text: `[time=${moment(new Date()).format('llll')}]`
|
||||
}
|
||||
default:
|
||||
return {
|
||||
text: suggestion + ' ',
|
||||
displayText: suggestion
|
||||
}
|
||||
}
|
||||
}),
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end + 1)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const LinkAndExtraTagHinter: Hinter = {
|
||||
allowedChars,
|
||||
wordRegExp,
|
||||
hint: linkAndExtraTagHint
|
||||
}
|
35
src/components/editor/editor-pane/autocompletion/pdf.ts
Normal file
35
src/components/editor/editor-pane/autocompletion/pdf.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
||||
import { findWordAtCursor, Hinter } from './index'
|
||||
|
||||
const allowedChars = /[{%]/
|
||||
const wordRegExp = /^({[%}]?)$/
|
||||
|
||||
const pdfHint = (editor: Editor): Promise< Hints| null > => {
|
||||
return new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor, allowedChars)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const suggestions = ['{%pdf https:// %}']
|
||||
const cursor = editor.getCursor()
|
||||
if (!suggestions) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: suggestions.map((suggestion: string): Hint => ({
|
||||
text: suggestion
|
||||
})),
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end + 1)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const PDFHinter: Hinter = {
|
||||
allowedChars,
|
||||
wordRegExp,
|
||||
hint: pdfHint
|
||||
}
|
|
@ -12,20 +12,20 @@ import 'codemirror/addon/edit/matchtags'
|
|||
import 'codemirror/addon/fold/foldcode'
|
||||
import 'codemirror/addon/fold/foldgutter'
|
||||
import 'codemirror/addon/hint/show-hint'
|
||||
import 'codemirror/addon/search/search'
|
||||
import 'codemirror/addon/search/jump-to-line'
|
||||
import 'codemirror/addon/search/match-highlighter'
|
||||
import 'codemirror/addon/search/search'
|
||||
import 'codemirror/addon/selection/active-line'
|
||||
import 'codemirror/keymap/sublime'
|
||||
import 'codemirror/keymap/emacs'
|
||||
import 'codemirror/keymap/sublime'
|
||||
import 'codemirror/keymap/vim'
|
||||
import 'codemirror/mode/gfm/gfm'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import './editor-pane.scss'
|
||||
import { ScrollProps, ScrollState } from '../scroll/scroll-props'
|
||||
import { generateEmojiHints, emojiWordRegex, findWordAtCursor } from './hints/emoji'
|
||||
import { allHinters, findWordAtCursor } from './autocompletion'
|
||||
import './editor-pane.scss'
|
||||
import { defaultKeyMap } from './key-map'
|
||||
import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './status-bar/status-bar'
|
||||
import { ToolBar } from './tool-bar/tool-bar'
|
||||
|
@ -35,17 +35,18 @@ export interface EditorPaneProps {
|
|||
content: string
|
||||
}
|
||||
|
||||
const hintOptions = {
|
||||
hint: generateEmojiHints,
|
||||
completeSingle: false,
|
||||
completeOnSingleClick: false,
|
||||
alignWithWord: true
|
||||
}
|
||||
|
||||
const onChange = (editor: Editor) => {
|
||||
const searchTerm = findWordAtCursor(editor)
|
||||
if (emojiWordRegex.test(searchTerm.text)) {
|
||||
editor.showHint(hintOptions)
|
||||
for (const hinter of allHinters) {
|
||||
const searchTerm = findWordAtCursor(editor, hinter.allowedChars)
|
||||
if (hinter.wordRegExp.test(searchTerm.text)) {
|
||||
editor.showHint({
|
||||
hint: hinter.hint,
|
||||
completeSingle: false,
|
||||
completeOnSingleClick: false,
|
||||
alignWithWord: true
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,3 +15,7 @@ export const clearUser: () => void = () => {
|
|||
}
|
||||
store.dispatch(action)
|
||||
}
|
||||
|
||||
export const getUser = (): UserState | null => {
|
||||
return store.getState().user
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue