mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-17 00:24: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,10 +8,82 @@ describe('Autocompletion', () => {
|
||||||
.type('{backspace}')
|
.type('{backspace}')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('code block', () => {
|
||||||
|
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', '```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('```')
|
||||||
|
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', () => {
|
describe('normal emoji', () => {
|
||||||
it('via Enter', () => {
|
it('via Enter', () => {
|
||||||
cy.get('.CodeMirror textarea')
|
cy.get('.CodeMirror textarea')
|
||||||
.type(':book')
|
.type(':book')
|
||||||
|
cy.get('.CodeMirror-hints')
|
||||||
|
.should('exist')
|
||||||
|
cy.get('.CodeMirror textarea')
|
||||||
.type('{enter}')
|
.type('{enter}')
|
||||||
cy.get('.CodeMirror-hints')
|
cy.get('.CodeMirror-hints')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
|
@ -39,6 +111,9 @@ describe('Autocompletion', () => {
|
||||||
it('via Enter', () => {
|
it('via Enter', () => {
|
||||||
cy.get('.CodeMirror textarea')
|
cy.get('.CodeMirror textarea')
|
||||||
.type(':facebook')
|
.type(':facebook')
|
||||||
|
cy.get('.CodeMirror-hints')
|
||||||
|
.should('exist')
|
||||||
|
cy.get('.CodeMirror textarea')
|
||||||
.type('{enter}')
|
.type('{enter}')
|
||||||
cy.get('.CodeMirror-hints')
|
cy.get('.CodeMirror-hints')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
|
@ -61,4 +136,133 @@ describe('Autocompletion', () => {
|
||||||
.should('exist')
|
.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', '# ')
|
||||||
|
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 { Editor, Hint, Hints, Pos } from 'codemirror'
|
||||||
import { Data, EmojiData, NimbleEmojiIndex } from 'emoji-mart'
|
import { Data, EmojiData, NimbleEmojiIndex } from 'emoji-mart'
|
||||||
import data from 'emoji-mart/data/twitter.json'
|
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 { customEmojis } from '../tool-bar/emoji-picker/emoji-picker'
|
||||||
|
import { getEmojiIcon, getEmojiShortCode } from '../tool-bar/utils/emojiUtils'
|
||||||
|
import { findWordAtCursor, Hinter } from './index'
|
||||||
|
|
||||||
interface findWordAtCursorResponse {
|
const allowedCharsInEmojiCodeRegex = /[:\w-_+]/
|
||||||
start: number,
|
|
||||||
end: number,
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowedCharsInEmojiCodeRegex = /(:|\w|-|_|\+)/
|
|
||||||
const emojiIndex = new NimbleEmojiIndex(data as unknown as Data)
|
const emojiIndex = new NimbleEmojiIndex(data as unknown as Data)
|
||||||
export const emojiWordRegex = /^:((\w|-|_|\+)+)$/
|
const emojiWordRegex = /^:([\w-_+]*)$/
|
||||||
|
|
||||||
export const findWordAtCursor = (editor: Editor): findWordAtCursorResponse => {
|
const generateEmojiHints = (editor: Editor): Promise< Hints| null > => {
|
||||||
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 > => {
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const searchTerm = findWordAtCursor(editor)
|
const searchTerm = findWordAtCursor(editor, allowedCharsInEmojiCodeRegex)
|
||||||
const searchResult = emojiWordRegex.exec(searchTerm.text)
|
const searchResult = emojiWordRegex.exec(searchTerm.text)
|
||||||
if (searchResult === null) {
|
if (searchResult === null) {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const term = searchResult[1]
|
const term = searchResult[1]
|
||||||
if (!term) {
|
let search: EmojiData[] | null = emojiIndex.search(term, {
|
||||||
resolve(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const search = emojiIndex.search(term, {
|
|
||||||
emojisToShowFilter: () => true,
|
emojisToShowFilter: () => true,
|
||||||
maxResults: 5,
|
maxResults: 7,
|
||||||
include: [],
|
include: [],
|
||||||
exclude: [],
|
exclude: [],
|
||||||
custom: customEmojis as EmojiData[]
|
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()
|
const cursor = editor.getCursor()
|
||||||
if (!search) {
|
if (!search) {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
} else {
|
} else {
|
||||||
resolve({
|
resolve({
|
||||||
list: search.map((emojiData: EmojiData): Hint => ({
|
list: search.map((emojiData): Hint => ({
|
||||||
text: getEmojiShortCode(emojiData),
|
text: getEmojiShortCode(emojiData),
|
||||||
render: (parent: HTMLLIElement) => {
|
render: (parent: HTMLLIElement) => {
|
||||||
const wrapper = document.createElement('div')
|
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/foldcode'
|
||||||
import 'codemirror/addon/fold/foldgutter'
|
import 'codemirror/addon/fold/foldgutter'
|
||||||
import 'codemirror/addon/hint/show-hint'
|
import 'codemirror/addon/hint/show-hint'
|
||||||
import 'codemirror/addon/search/search'
|
|
||||||
import 'codemirror/addon/search/jump-to-line'
|
import 'codemirror/addon/search/jump-to-line'
|
||||||
import 'codemirror/addon/search/match-highlighter'
|
import 'codemirror/addon/search/match-highlighter'
|
||||||
|
import 'codemirror/addon/search/search'
|
||||||
import 'codemirror/addon/selection/active-line'
|
import 'codemirror/addon/selection/active-line'
|
||||||
import 'codemirror/keymap/sublime'
|
|
||||||
import 'codemirror/keymap/emacs'
|
import 'codemirror/keymap/emacs'
|
||||||
|
import 'codemirror/keymap/sublime'
|
||||||
import 'codemirror/keymap/vim'
|
import 'codemirror/keymap/vim'
|
||||||
import 'codemirror/mode/gfm/gfm'
|
import 'codemirror/mode/gfm/gfm'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, 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-pane.scss'
|
|
||||||
import { ScrollProps, ScrollState } from '../scroll/scroll-props'
|
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 { defaultKeyMap } from './key-map'
|
||||||
import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './status-bar/status-bar'
|
import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './status-bar/status-bar'
|
||||||
import { ToolBar } from './tool-bar/tool-bar'
|
import { ToolBar } from './tool-bar/tool-bar'
|
||||||
|
@ -35,17 +35,18 @@ export interface EditorPaneProps {
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const hintOptions = {
|
const onChange = (editor: Editor) => {
|
||||||
hint: generateEmojiHints,
|
for (const hinter of allHinters) {
|
||||||
|
const searchTerm = findWordAtCursor(editor, hinter.allowedChars)
|
||||||
|
if (hinter.wordRegExp.test(searchTerm.text)) {
|
||||||
|
editor.showHint({
|
||||||
|
hint: hinter.hint,
|
||||||
completeSingle: false,
|
completeSingle: false,
|
||||||
completeOnSingleClick: false,
|
completeOnSingleClick: false,
|
||||||
alignWithWord: true
|
alignWithWord: true
|
||||||
}
|
})
|
||||||
|
return
|
||||||
const onChange = (editor: Editor) => {
|
}
|
||||||
const searchTerm = findWordAtCursor(editor)
|
|
||||||
if (emojiWordRegex.test(searchTerm.text)) {
|
|
||||||
editor.showHint(hintOptions)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,3 +15,7 @@ export const clearUser: () => void = () => {
|
||||||
}
|
}
|
||||||
store.dispatch(action)
|
store.dispatch(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getUser = (): UserState | null => {
|
||||||
|
return store.getState().user
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue