mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 04:39:47 +08:00
Merge pull request #1572 from marktext/code-block-line-numbers
feat: add code block line numbers
This commit is contained in:
commit
dfc73900aa
@ -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 |
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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: '-',
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -205,7 +205,7 @@ class ContentState {
|
||||
}
|
||||
|
||||
postRender () {
|
||||
// do nothing.
|
||||
this.resizeLineNumber()
|
||||
}
|
||||
|
||||
render (isRenderCursor = true, clearCache = false) {
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
}
|
||||
|
22
src/muya/lib/eventHandler/resize.js
Normal file
22
src/muya/lib/eventHandler/resize.js
Normal 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
|
@ -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) {
|
||||
|
@ -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) {
|
||||
selector += `.language-${lang.replace(/[#.]{1}/g, '')}`
|
||||
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' })
|
||||
}
|
||||
|
||||
|
21
src/muya/lib/parser/render/renderBlock/renderCopyButton.js
Normal file
21
src/muya/lib/parser/render/renderBlock/renderCopyButton.js
Normal 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
|
22
src/muya/lib/parser/render/renderBlock/renderLineNumber.js
Normal file
22
src/muya/lib/parser/render/renderBlock/renderLineNumber.js
Normal 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
|
57
src/muya/lib/utils/resizeCodeLineNumber.js
Normal file
57
src/muya/lib/utils/resizeCodeLineNumber.js
Normal 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
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -20,6 +20,7 @@ const state = {
|
||||
lineHeight: 1.6,
|
||||
codeFontSize: 14,
|
||||
codeFontFamily: 'DejaVu Sans Mono',
|
||||
codeBlockLineNumbers: true,
|
||||
trimUnnecessaryCodeBlockEmptyLines: true,
|
||||
editorLineWidth: '',
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
"lineHeight": 1.6,
|
||||
"codeFontSize": 14,
|
||||
"codeFontFamily": "DejaVu Sans Mono",
|
||||
"codeBlockLineNumbers": true,
|
||||
"trimUnnecessaryCodeBlockEmptyLines": true,
|
||||
"editorLineWidth": "",
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user