Merge pull request #1572 from marktext/code-block-line-numbers

feat: add code block line numbers
This commit is contained in:
Ran Luo 2019-11-06 22:27:06 +08:00 committed by GitHub
commit dfc73900aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 307 additions and 8 deletions

View File

@ -31,6 +31,7 @@ Preferences can be controlled and modified in the settings window or via the `pr
| textDirection | String | ltr | The writing text direction, optional value: `ltr` or `rtl` |
| codeFontSize | Number | 14 | Font size on code block, the range is 12 ~ 28 |
| codeFontFamily | String | `DejaVu Sans Mono` | Code font family |
| codeBlockLineNumbers | Boolean | true | Whether to show the line numbers in code block |
| trimUnnecessaryCodeBlockEmptyLines | Boolean | true | Whether to trim the beginning and end empty line in Code block |
| hideQuickInsertHint | Boolean | false | Hide hint for quickly creating paragraphs |
| imageDropAction | String | folder | The default behavior after paste or drag the image to Mark Text, upload it to the image cloud (if configured), move to the specified folder, insert the path |

View File

@ -94,6 +94,11 @@
"type": "string",
"pattern": "^[_A-z0-9]+((-|\\s)*[_A-z0-9])*$"
},
"codeBlockLineNumbers": {
"description": "Editor--Whether to show the line numbers",
"type": "boolean",
"default": true
},
"trimUnnecessaryCodeBlockEmptyLines": {
"description": "Editor--Trim the beginning and ending empty lines in code block",
"type": "boolean"

View File

@ -582,7 +582,7 @@ pre.ag-front-matter span.ag-code-content:first-of-type:empty::after {
}
pre[data-role$='code'] span.ag-language-input:empty::after {
content: 'Input Language...';
content: 'Input Language Identifier...';
color: var(--editorColor10);
}
@ -621,7 +621,7 @@ pre.ag-indent-code > code::before,
pre.ag-fence-code > code::before {
content: '';
position: absolute;
top: -10px;
bottom: -1em;
right: -5px;
color: var(--editorColor30);
font-size: 12px;
@ -1157,6 +1157,97 @@ span.ag-reference-link {
padding-right: 0;
}
.ag-code-copy {
position: absolute;
top: .5em;
right: .5em;
width: 20px;
height: 20px;
border-radius: 4px;
cursor: pointer;
opacity: 0;
z-index: 1;
transition: opacity .2s ease-in-out;
}
.ag-active .ag-code-copy {
opacity: .5;
}
pre:not(.ag-active):hover .ag-code-copy {
opacity: .5;
}
.ag-code-copy:hover {
opacity: 1;
}
.ag-code-copy i.icon {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
}
.ag-code-copy i.icon > i[class^=icon-] {
width: 100%;
height: 100%;
filter: drop-shadow(16px 0 var(--iconColor));
position: absolute;
left: -16px;
}
pre.ag-paragraph.line-numbers {
position: relative;
padding-left: 2.5em;
counter-reset: linenumber;
}
pre.ag-paragraph.line-numbers > code {
position: relative;
white-space: inherit;
}
figure:not(.ag-active) pre.ag-paragraph.line-numbers {
display: none;
}
.line-numbers .line-numbers-rows {
position: absolute;
pointer-events: none;
top: 0;
font-size: 100%;
left: -2.5em;
width: 2.5em; /* works for line-numbers below 1000 lines */
letter-spacing: -1px;
user-select: none;
}
.line-numbers-sizer {
white-space: pre-line;
word-break: break-all;
}
.line-numbers-rows > span {
pointer-events: none;
display: block;
counter-increment: linenumber;
}
.line-numbers-rows > span:before {
content: counter(linenumber);
color: var(--editorColor30);
display: block;
padding-right: .8em;
text-align: right;
transform: scale(.8);
position: relative;
top: .05em;
}
.ag-inline-footnote-identifier {
background: var(--codeBlockBgColor);
padding: 0 0.4em;

View File

@ -255,6 +255,7 @@ export const MUYA_DEFAULT_OPTION = {
bulletListMarker: '-',
orderListDelimiter: '.',
tabSize: 4,
codeBlockLineNumbers: true,
// bullet/list marker width + listIndentation, tab or Daring Fireball Markdown (4 spaces) --> list indentation
listIndentation: 1,
frontmatterType: '-',

View File

@ -1,4 +1,5 @@
import { loadLanguage } from '../prism/index'
import resizeCodeBlockLineNumber from '../utils/resizeCodeLineNumber'
import selection from '../selection'
const CODE_UPDATE_REP = /^`{3,}(.*)/
@ -123,6 +124,32 @@ const codeBlockCtrl = ContentState => {
}
return false
}
/**
* Copy the code block by click right-top copy icon in code block.
*/
ContentState.prototype.copyCodeBlock = function (event) {
const { target } = event
const preEle = target.closest('pre')
const preBlock = this.getBlock(preEle.id)
const codeBlock = preBlock.children.find(c => c.type === 'code')
const codeContent = codeBlock.children[0].text
this.muya.clipboard.copy('copyCodeContent', codeContent)
}
ContentState.prototype.resizeLineNumber = function () {
const { codeBlockLineNumbers } = this.muya.options
if (!codeBlockLineNumbers) {
return
}
const codeBlocks = document.querySelectorAll('pre.line-numbers')
if (codeBlocks.length) {
for (const ele of codeBlocks) {
resizeCodeBlockLineNumber(ele)
}
}
}
}
export default codeBlockCtrl

View File

@ -47,6 +47,9 @@ const copyCutCtrl = ContentState => {
ContentState.prototype.getClipBoradData = function () {
const { start, end } = selection.getCursorRange()
if (!start || !end) {
return { html: '', text: '' }
}
if (start.key === end.key) {
const startBlock = this.getBlock(start.key)
const { type, text, functionType } = startBlock
@ -273,6 +276,15 @@ const copyCutCtrl = ContentState => {
event.clipboardData.setData('text/plain', markdown)
break
}
case 'copyCodeContent': {
const codeContent = copyInfo
if (typeof codeContent !== 'string') {
return
}
event.clipboardData.setData('text/html', '')
event.clipboardData.setData('text/plain', codeContent)
}
}
}
}

View File

@ -205,7 +205,7 @@ class ContentState {
}
postRender () {
// do nothing.
this.resizeLineNumber()
}
render (isRenderCursor = true, clearCache = false) {

View File

@ -100,6 +100,7 @@ class ClickEvent {
const mathRender = target.closest(`.${CLASS_OR_ID.AG_MATH_RENDER}`)
const rubyRender = target.closest(`.${CLASS_OR_ID.AG_RUBY_RENDER}`)
const imageWrapper = target.closest(`.${CLASS_OR_ID.AG_INLINE_IMAGE}`)
const codeCopy = target.closest('.ag-code-copy')
const footnoteBackLink = target.closest('.ag-footnote-backlink')
const imageDelete = target.closest('.ag-image-icon-delete') || target.closest('.ag-image-icon-close')
const mathText = mathRender && mathRender.previousElementSibling
@ -116,6 +117,11 @@ class ClickEvent {
} else if (rubyText) {
selectionText(rubyText)
}
if (codeCopy) {
event.stopPropagation()
event.preventDefault()
return this.muya.contentState.copyCodeBlock(event)
}
// Handle delete inline iamge by click delete icon.
if (imageDelete && imageWrapper) {
const imageInfo = getImageInfo(imageWrapper)

View File

@ -60,10 +60,11 @@ class Clipboard {
/**
* Copy the anchor block(table, paragraph, math block etc) with the info
* @param {string|object} type copyBlock or copyCodeContent
* @param {string|object} info is the block key if it's string, or block if it's object
*/
copy (info) {
this._copyType = 'copyBlock'
copy (type, info) {
this._copyType = type
this._copyInfo = info
document.execCommand('copy')
}

View File

@ -0,0 +1,22 @@
import resizeCodeBlockLineNumber from '../utils/resizeCodeLineNumber'
import { throttle } from '../utils'
class Resize {
constructor (muya) {
this.muya = muya
this.listen()
}
listen () {
window.addEventListener('resize', throttle(() => {
const codeBlocks = document.querySelectorAll('pre.line-numbers')
if (codeBlocks.length) {
for (const ele of codeBlocks) {
resizeCodeBlockLineNumber(ele)
}
}
}, 300))
}
}
export default Resize

View File

@ -4,6 +4,7 @@ import MouseEvent from './eventHandler/mouseEvent'
import Clipboard from './eventHandler/clipboard'
import Keyboard from './eventHandler/keyboard'
import DragDrop from './eventHandler/dragDrop'
import Resize from './eventHandler/resize'
import ClickEvent from './eventHandler/clickEvent'
import { CLASS_OR_ID, MUYA_DEFAULT_OPTION } from './config'
import { wordCount } from './utils'
@ -41,6 +42,7 @@ class Muya {
this.clickEvent = new ClickEvent(this)
this.keyboard = new Keyboard(this)
this.dragdrop = new DragDrop(this)
this.resize = new Resize(this)
this.mouseEvent = new MouseEvent(this)
this.init()
}
@ -362,7 +364,7 @@ class Muya {
* @param {string|object} key the block key or block
*/
copy (info) {
return this.clipboard.copy(info)
return this.clipboard.copy('copyBlock', info)
}
setOptions (options, needRender = false) {

View File

@ -2,6 +2,8 @@ import { CLASS_OR_ID } from '../../../config'
import { renderTableTools } from './renderToolBar'
import { footnoteJumpIcon } from './renderFootnoteJump'
import { renderEditIcon } from './renderContainerEditIcon'
import renderLineNumberRows from './renderLineNumber'
import renderCopyButton from './renderCopyButton'
import { renderLeftBar, renderBottomBar } from './renderTableDargBar'
import { h } from '../snabbdom'
@ -46,8 +48,20 @@ export default function renderContainerBlock (parent, block, activeBlocks, match
})
}
if (/code|pre/.test(type) && typeof lang === 'string' && !!lang) {
if (/code|pre/.test(type)) {
if (typeof lang === 'string' && !!lang) {
selector += `.language-${lang.replace(/[#.]{1}/g, '')}`
}
if (type === 'pre') {
children.unshift(renderCopyButton())
}
if (this.muya.options.codeBlockLineNumbers) {
if (type === 'pre') {
selector += '.line-numbers'
} else {
children.unshift(renderLineNumberRows(block.children[0]))
}
}
Object.assign(data.attrs, { spellcheck: 'false' })
}

View File

@ -0,0 +1,21 @@
import { h } from '../snabbdom'
import copyIcon from '../../../assets/pngicon/copy/2.png'
const renderCopyButton = () => {
const selector = 'a.ag-code-copy'
const iconVnode = h('i.icon', h('i.icon-inner', {
style: {
background: `url(${copyIcon}) no-repeat`,
'background-size': '100%'
}
}, ''))
return h(selector, {
attrs: {
title: 'Copy content',
contenteditable: 'false'
}
}, iconVnode)
}
export default renderCopyButton

View File

@ -0,0 +1,22 @@
import { h } from '../snabbdom'
const NEW_LINE_EXP = /\n(?!$)/g
const renderLineNumberRows = codeContent => {
const { text } = codeContent
const match = text.match(NEW_LINE_EXP)
let linesNum = match ? match.length + 1 : 1
if (text.endsWith('\n')) {
linesNum++
}
const data = {
attrs: {
'aria-hidden': true
}
}
const children = [...new Array(linesNum)].map(() => h('span'))
return h('span.line-numbers-rows', data, children)
}
export default renderLineNumberRows

View File

@ -0,0 +1,57 @@
/**
* This file copy from prismjs/plugins/prism-line-number
*/
/**
* Regular expression used for determining line breaks
* @type {RegExp}
*/
const NEW_LINE_EXP = /\n(?!$)/g
/**
* Returns style declarations for the element
* @param {Element} element
*/
const getStyles = function (element) {
if (!element) {
return null
}
return window.getComputedStyle ? getComputedStyle(element) : (element.currentStyle || null)
}
/**
* Resizes line numbers spans according to height of line of code
* @param {Element} element <pre> element
*/
const resizeCodeBlockLineNumber = function (element) {
const codeStyles = getStyles(element)
const whiteSpace = codeStyles['white-space']
if (whiteSpace === 'pre' || whiteSpace === 'pre-wrap' || whiteSpace === 'pre-line') {
const codeElement = element.querySelector('code')
const lineNumbersWrapper = element.querySelector('.line-numbers-rows')
let lineNumberSizer = element.querySelector('.line-numbers-sizer')
const codeLines = codeElement.textContent.split(NEW_LINE_EXP)
if (!lineNumberSizer) {
lineNumberSizer = document.createElement('span')
lineNumberSizer.className = 'line-numbers-sizer'
codeElement.appendChild(lineNumberSizer)
}
lineNumberSizer.style.display = 'block'
codeLines.forEach(function (line, lineNumber) {
lineNumberSizer.textContent = line || '\n'
const lineSize = lineNumberSizer.getBoundingClientRect().height
lineNumbersWrapper.children[lineNumber].style.height = lineSize + 'px'
})
lineNumberSizer.textContent = ''
lineNumberSizer.style.display = 'none'
}
}
export default resizeCodeBlockLineNumber

View File

@ -142,6 +142,7 @@ export default {
fontSize: state => state.preferences.fontSize,
codeFontSize: state => state.preferences.codeFontSize,
codeFontFamily: state => state.preferences.codeFontFamily,
codeBlockLineNumbers: state => state.preferences.codeBlockLineNumbers,
trimUnnecessaryCodeBlockEmptyLines: state => state.preferences.trimUnnecessaryCodeBlockEmptyLines,
editorFontFamily: state => state.preferences.editorFontFamily,
hideQuickInsertHint: state => state.preferences.hideQuickInsertHint,
@ -320,6 +321,12 @@ export default {
})
}
},
codeBlockLineNumbers: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setOptions({ codeBlockLineNumbers: value }, true)
}
},
codeFontFamily: function (value, oldValue) {
if (value !== oldValue) {
addCommonStyle({
@ -451,6 +458,7 @@ export default {
tabSize,
fontSize,
lineHeight,
codeBlockLineNumbers,
listIndentation,
frontmatterType,
superSubScript,
@ -495,6 +503,7 @@ export default {
tabSize,
fontSize,
lineHeight,
codeBlockLineNumbers,
listIndentation,
frontmatterType,
superSubScript,

View File

@ -39,6 +39,11 @@
:value="codeFontFamily"
:onChange="value => onSelectChange('codeFontFamily', value)"
></font-text-box>
<bool
description="Whether to show the code block line numbers."
:bool="codeBlockLineNumbers"
:onChange="value => onSelectChange('codeBlockLineNumbers', value)"
></bool>
<bool
description="Trim the beginning and ending empty lines in code block when open markdown."
:bool="trimUnnecessaryCodeBlockEmptyLines"
@ -148,6 +153,7 @@ export default {
textDirection: state => state.preferences.textDirection,
codeFontSize: state => state.preferences.codeFontSize,
codeFontFamily: state => state.preferences.codeFontFamily,
codeBlockLineNumbers: state => state.preferences.codeBlockLineNumbers,
trimUnnecessaryCodeBlockEmptyLines: state => state.preferences.trimUnnecessaryCodeBlockEmptyLines,
hideQuickInsertHint: state => state.preferences.hideQuickInsertHint,
hideLinkPopup: state => state.preferences.hideLinkPopup,

View File

@ -20,6 +20,7 @@ const state = {
lineHeight: 1.6,
codeFontSize: 14,
codeFontFamily: 'DejaVu Sans Mono',
codeBlockLineNumbers: true,
trimUnnecessaryCodeBlockEmptyLines: true,
editorLineWidth: '',

View File

@ -16,6 +16,7 @@
"lineHeight": 1.6,
"codeFontSize": 14,
"codeFontFamily": "DejaVu Sans Mono",
"codeBlockLineNumbers": true,
"trimUnnecessaryCodeBlockEmptyLines": true,
"editorLineWidth": "",