diff --git a/src/muya/lib/assets/pngicon/footnote/2.png b/src/muya/lib/assets/pngicon/footnote/2.png new file mode 100644 index 00000000..f5be4b5e Binary files /dev/null and b/src/muya/lib/assets/pngicon/footnote/2.png differ diff --git a/src/muya/lib/assets/styles/index.css b/src/muya/lib/assets/styles/index.css index d20c659d..4f8d4ffc 100644 --- a/src/muya/lib/assets/styles/index.css +++ b/src/muya/lib/assets/styles/index.css @@ -180,6 +180,56 @@ figure[data-role="HTML"].ag-active .ag-html-preview { display: none; } +figure[data-role="FOOTNOTE"] { + position: relative; + background: var(--footnoteBgColor); + padding: .5em 1em; + font-size: .8em; + opacity: .8; + border-radius: 3px; +} + +figure[data-role="FOOTNOTE"].ag-active::before { + content: attr(data-role); + text-transform: lowercase; + position: absolute; + top: 15px; + right: 10px; + color: var(--editorColor30); + font-size: 12px; +} + +figure[data-role="FOOTNOTE"] pre { + font-size: .8em; +} + +figure[data-role="FOOTNOTE"] .ag-footnote-input { + padding: 0 1em; + min-width: 80px; + position: absolute; + top: -23px; + left: 0; + font-size: 14px; + font-family: monospace; + font-weight: 600; + color: var(--editorColor); + background: transparent; + z-index: 1; + display: none; +} + +figure[data-role="FOOTNOTE"].ag-active .ag-footnote-input { + display: inline-block; +} + +figure[data-role="FOOTNOTE"] .ag-footnote-input::before { + content: '[^'; +} + +figure[data-role="FOOTNOTE"] .ag-footnote-input::after { + content: ']:'; +} + .ag-highlight { animation-name: highlight; animation-duration: .25s; diff --git a/src/muya/lib/contentState/footnoteCtrl.js b/src/muya/lib/contentState/footnoteCtrl.js new file mode 100644 index 00000000..94f8f366 --- /dev/null +++ b/src/muya/lib/contentState/footnoteCtrl.js @@ -0,0 +1,43 @@ +/* eslint-disable no-useless-escape */ +const FOOTNOTE_REG = /^\[\^([^\^\[\]\s]+?)(? { + ContentState.prototype.updateFootnote = function (block, line) { + const { start, end } = this.cursor + const { text } = line + const match = FOOTNOTE_REG.exec(text) + const footnoteIdentifer = match[1] + const sectionWrapper = this.createBlock('figure', { + functionType: 'footnote' + }) + const footnoteInput = this.createBlock('span', { + text: footnoteIdentifer, + functionType: 'footnoteInput' + }) + const pBlock = this.createBlockP(text.substring(match[0].length)) + this.appendChild(sectionWrapper, footnoteInput) + this.appendChild(sectionWrapper, pBlock) + this.insertBefore(sectionWrapper, block) + this.removeBlock(block) + + const { key } = pBlock.children[0] + this.cursor = { + start: { + key, + offset: Math.max(0, start.offset - footnoteIdentifer.length) + }, + end: { + key, + offset: Math.max(0, end.offset - footnoteIdentifer.length) + } + } + + if (this.isCollapse()) { + this.checkInlineUpdate(pBlock.children[0]) + } + + return this.partialRender() + } +} + +export default footnoteCtrl diff --git a/src/muya/lib/contentState/index.js b/src/muya/lib/contentState/index.js index 47ce4214..735dd93b 100644 --- a/src/muya/lib/contentState/index.js +++ b/src/muya/lib/contentState/index.js @@ -28,6 +28,7 @@ import emojiCtrl from './emojiCtrl' import imageCtrl from './imageCtrl' import linkCtrl from './linkCtrl' import dragDropCtrl from './dragDropCtrl' +import footnoteCtrl from './footnoteCtrl' import importMarkdown from '../utils/importMarkdown' import Cursor from '../selection/cursor' import escapeCharactersMap, { escapeCharacters } from '../parser/escapeCharacter' @@ -58,6 +59,7 @@ const prototypes = [ imageCtrl, linkCtrl, dragDropCtrl, + footnoteCtrl, importMarkdown ] diff --git a/src/muya/lib/contentState/updateCtrl.js b/src/muya/lib/contentState/updateCtrl.js index fadccfab..baedb065 100644 --- a/src/muya/lib/contentState/updateCtrl.js +++ b/src/muya/lib/contentState/updateCtrl.js @@ -10,6 +10,7 @@ const INLINE_UPDATE_FRAGMENTS = [ '^(?:[\\s\\S]+?)\\n {0,3}(\\={3,}|\\-{3,})(?= {1,}|$)', // Setext headings **match from beginning** '(?:^|\n) {0,3}(>).+', // Block quote '^( {4,})', // Indent code **match from beginning** + '^(\\[\\^[^\\^\\[\\]\\s]+?(? { if (/figure/.test(block.type)) { return false } - if (/cellContent|codeContent|languageInput/.test(block.functionType)) { + if (/cellContent|codeContent|languageInput|footnoteInput/.test(block.functionType)) { return false } @@ -89,8 +90,9 @@ const updateCtrl = ContentState => { const listItem = this.getParent(block) const [ match, bullet, tasklist, order, atxHeader, - setextHeader, blockquote, indentCode, hr + setextHeader, blockquote, indentCode, footnote, hr ] = text.match(INLINE_UPDATE_REG) || [] + const { footnote: isSupportFootnote } = this.muya.options switch (true) { case (!!hr && new Set(hr.split('').filter(i => /\S/.test(i))).size === 1): @@ -118,6 +120,9 @@ const updateCtrl = ContentState => { case !!indentCode: return this.updateIndentCode(block, line) + case !!footnote && block.type === 'p' && !block.parent && isSupportFootnote: + return this.updateFootnote(block, line) + case !match: default: return this.updateToParagraph(block, line) diff --git a/src/muya/lib/parser/render/renderBlock/renderIcon.js b/src/muya/lib/parser/render/renderBlock/renderIcon.js index 761e1e43..bd9ae3e0 100644 --- a/src/muya/lib/parser/render/renderBlock/renderIcon.js +++ b/src/muya/lib/parser/render/renderBlock/renderIcon.js @@ -21,6 +21,7 @@ import flowchartIcon from '../../../assets/pngicon/flowchart/2.png' import sequenceIcon from '../../../assets/pngicon/sequence/2.png' import mermaidIcon from '../../../assets/pngicon/mermaid/2.png' import vegaIcon from '../../../assets/pngicon/chart/2.png' +import footnoteIcon from '../../../assets/pngicon/footnote/2.png' const FUNCTION_TYPE_HASH = { mermaid: mermaidIcon, @@ -32,7 +33,8 @@ const FUNCTION_TYPE_HASH = { multiplemath: mathblockIcon, fencecode: codeIcon, indentcode: codeIcon, - frontmatter: frontMatterIcon + frontmatter: frontMatterIcon, + footnote: footnoteIcon } export default function renderIcon (block) { diff --git a/src/muya/lib/parser/rules.js b/src/muya/lib/parser/rules.js index ff2deb49..070a817f 100644 --- a/src/muya/lib/parser/rules.js +++ b/src/muya/lib/parser/rules.js @@ -42,6 +42,6 @@ export const inlineExtensionRules = { // This is not the best regexp, because it not support `2^2\\^`. superscript: /^(\^)((?:[^\^\s]|(?<=\\)\1|(?<=\\) )+?)(?