fix conflict

This commit is contained in:
jocs 2019-11-03 10:58:13 +08:00
commit 4726553a34
86 changed files with 1761 additions and 943 deletions

View File

@ -51,7 +51,7 @@ const rendererConfig = {
}
},
{
test: /(theme\-chalk(?:\/|\\)index|katex|github\-markdown|prism[\-a-z]*|\.theme|headerFooterStyle)\.css$/,
test: /(theme\-chalk(?:\/|\\)index|exportStyle|katex|github\-markdown|prism[\-a-z]*|\.theme|headerFooterStyle)\.css$/,
use: [
'to-string-loader',
'css-loader'
@ -59,7 +59,7 @@ const rendererConfig = {
},
{
test: /\.css$/,
exclude: /(theme\-chalk(?:\/|\\)index|katex|github\-markdown|prism[\-a-z]*|\.theme|headerFooterStyle)\.css$/,
exclude: /(theme\-chalk(?:\/|\\)index|exportStyle|katex|github\-markdown|prism[\-a-z]*|\.theme|headerFooterStyle)\.css$/,
use: [
proMode ? MiniCssExtractPlugin.loader : 'style-loader',
{ loader: 'css-loader', options: { importLoaders: 1 } },

View File

@ -51,6 +51,7 @@ Preferences can be controlled and modified in the settings window or via the `pr
| listIndentation | String | 1 | The list indentation of sub list items or paragraphs, optional value `dfm`, `tab` or number 1~4 |
| frontmatterType | String | `-` | The frontmatter type: `-` (YAML), `+` (TOML), `;` (JSON) or `{` (JSON) |
| superSubScript | Boolean | `false` | Enable pandoc's markdown extension superscript and subscript. |
| footnote | Boolean | `false` | Enable pandoc's footnote markdown extension |
#### Theme

View File

@ -6,6 +6,8 @@
- languageInput
- footnoteInput
- codeContent (used in code block)
- cellContent (used in table cell, it's parent must be th or td block)
@ -46,6 +48,8 @@ The container block of `table`, `html`, `block math`, `mermaid`,`flowchart`,`veg
- table
- footnote
- html
- multiplemath

View File

@ -19,6 +19,7 @@ files:
- "!node_modules/vega-lite/build/vega-lite*.js.map"
# Don't bundle build files
- "!node_modules/@felixrieseberg/spellchecker/bin"
- "!node_modules/@hfelix/spellchecker/bin"
- "!node_modules/ced/bin"
- "!node_modules/ced/vendor"
- "!node_modules/cld/bin"
@ -34,6 +35,7 @@ files:
- "!node_modules/ced/build/vendor"
# Don't bundle LGPL source files
- "!node_modules/@felixrieseberg/spellchecker/vendor"
- "!node_modules/@hfelix/spellchecker/vendor"
extraFiles:
- "LICENSE"
- from: "resources/THIRD-PARTY-LICENSES.txt"

View File

@ -34,7 +34,7 @@
},
"dependencies": {
"@hfelix/electron-localshortcut": "^3.1.1",
"@hfelix/electron-spellchecker": "^1.0.0-rc.1",
"@hfelix/electron-spellchecker": "^1.0.0-rc.3",
"@octokit/rest": "^16.33.1",
"arg": "^4.1.1",
"axios": "^0.19.0",
@ -65,7 +65,7 @@
"joplin-turndown-plugin-gfm": "^1.0.11",
"katex": "^0.11.1",
"keyboard-layout": "^2.0.16",
"keytar": "^5.0.0-beta.3",
"keytar": "5.0.0-beta.4",
"mermaid": "^8.4.0",
"plist": "^3.0.1",
"popper.js": "^1.16.0",
@ -113,8 +113,8 @@
"del": "^5.1.0",
"devtron": "^1.4.0",
"dotenv": "^8.2.0",
"electron": "^6.1.0",
"electron-builder": "^21.2.0",
"electron": "7.0.0",
"electron-builder": "^22.1.0",
"electron-devtools-installer": "^2.2.4",
"electron-rebuild": "^1.8.6",
"electron-updater": "^4.1.2",
@ -153,7 +153,7 @@
"postcss-preset-env": "^6.6.0",
"raw-loader": "^3.1.0",
"require-dir": "^1.2.0",
"spectron": "^8.0.0",
"spectron": "^9.0.0",
"style-loader": "^1.0.0",
"svg-sprite-loader": "^4.1.6",
"svgo": "^1.3.0",
@ -171,9 +171,6 @@
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^4.2.1"
},
"optionalDependencies": {
"vscode-windows-registry": "^1.0.2"
},
"repository": {
"type": "git",
"url": "git@github.com:marktext/marktext.git"

View File

@ -3,7 +3,7 @@ import fse from 'fs-extra'
import { exec } from 'child_process'
import dayjs from 'dayjs'
import log from 'electron-log'
import { app, BrowserWindow, clipboard, dialog, ipcMain, systemPreferences } from 'electron'
import { app, BrowserWindow, clipboard, dialog, ipcMain, nativeTheme } from 'electron'
import { isChildOfDirectory } from 'common/filesystem/paths'
import { isLinux, isOsx, isWindows } from '../config'
import parseArgs from '../cli/parser'
@ -115,7 +115,7 @@ class App {
const { paths } = this._accessor
ensureDefaultDict(paths.userDataPath)
.catch(error => {
log.error(error)
log.error('Error copying Hunspell dictionary: ', error)
})
}
@ -143,7 +143,13 @@ class App {
}
}
const { startUpAction, defaultDirectoryToOpen } = preferences.getAll()
const {
startUpAction,
defaultDirectoryToOpen,
autoSwitchTheme,
theme
} = preferences.getAll()
if (startUpAction === 'folder' && defaultDirectoryToOpen) {
const info = normalizeMarkdownPath(defaultDirectoryToOpen)
if (info) {
@ -151,29 +157,32 @@ class App {
}
}
// Set initial native theme for theme in preferences.
const isDarkTheme = /dark/i.test(theme)
if (autoSwitchTheme === 0 && isDarkTheme !== nativeTheme.shouldUseDarkColors) {
selectTheme(nativeTheme.shouldUseDarkColors ? 'dark' : 'light')
nativeTheme.themeSource = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'
} else {
nativeTheme.themeSource = isDarkTheme ? 'dark' : 'light'
}
let isDarkMode = nativeTheme.shouldUseDarkColors
ipcMain.on('broadcast-preferences-changed', change => {
// Set Chromium's color for native elements after theme change.
if (change.theme) {
const isDarkTheme = /dark/i.test(change.theme)
if (isDarkMode !== isDarkTheme) {
isDarkMode = isDarkTheme
nativeTheme.themeSource = isDarkTheme ? 'dark' : 'light'
} else if (nativeTheme.themeSource === 'system') {
// Need to set dark or light theme because we set `system` to get the current system theme.
nativeTheme.themeSource = isDarkMode ? 'dark' : 'light'
}
}
})
if (isOsx) {
app.dock.setMenu(dockMenu)
// Listen for system theme change and change Mark Text own `dark` and `light`.
// In macOS 10.14 Mojave, Apple introduced a new system-wide dark mode for
// all macOS computers.
systemPreferences.subscribeNotification(
'AppleInterfaceThemeChangedNotification',
() => {
const preferences = this._accessor.preferences
const { theme } = preferences.getAll()
// Application menu is automatically updated via preference manager.
if (systemPreferences.isDarkMode() && theme !== 'dark' &&
theme !== 'material-dark' && theme !== 'one-dark') {
selectTheme('dark')
}
if (!systemPreferences.isDarkMode() && theme !== 'light' &&
theme !== 'ulysses' && theme !== 'graphite') {
selectTheme('light')
}
}
)
} else if (isWindows) {
app.setJumpList([{
type: 'recent'

View File

@ -16,7 +16,7 @@ export const editorWinOptions = {
zoomFactor: 1.0
}
export const defaultPreferenceWinOptions = {
export const preferencesWinOptions = {
width: 950,
height: 650,
webPreferences: {

View File

@ -71,7 +71,7 @@ class DataCenter extends EventEmitter {
return Object.assign(data, encryptObj)
} catch (err) {
log.error(err)
log.error('Failed to decrypt secure keys:', err)
return data
}
}
@ -133,7 +133,7 @@ class DataCenter extends EventEmitter {
try {
return await keytar.setPassword(serviceName, key, value)
} catch (err) {
log.error(err)
log.error('dataCenter::setItem:', err)
}
} else {
return this.store.set(key, value)

View File

@ -235,7 +235,7 @@ class Watcher {
})
}
} else {
log.error(error)
log.error('Error while watching files:', error)
}
})

View File

@ -60,9 +60,7 @@ try {
// Catch errors that may come from invalid configuration files like settings.
const msgHint = err.message.includes('Config schema violation')
? 'This seems to be an issue with your configuration file(s). ' : ''
log.error(`Loading Mark Text failed during initialization! ${msgHint}`)
log.error(err)
log.error(`Loading Mark Text failed during initialization! ${msgHint}`, err)
const EXIT_ON_ERROR = !!process.env.MARKTEXT_EXIT_ON_ERROR
const SHOW_ERROR_DIALOG = !process.env.MARKTEXT_ERROR_INTERACTION

View File

@ -62,7 +62,7 @@ const handleResponseForExport = async (e, { type, content, pathname, title, page
}
win.webContents.send('AGANI::export-success', { type, filePath })
} catch (err) {
log.error(err)
log.error('Error while exporting:', err)
const ERROR_MSG = err.message || `Error happened when export ${filePath}`
win.webContents.send('AGANI::show-notification', {
title: 'Export failure',
@ -80,19 +80,9 @@ const handleResponseForExport = async (e, { type, content, pathname, title, page
const handleResponseForPrint = e => {
const win = BrowserWindow.fromWebContents(e.sender)
// See GH#749, Electron#16085 and Electron#17523.
dialog.showMessageBox(win, {
type: 'info',
buttons: ['OK'],
defaultId: 0,
noLink: true,
message: 'Printing doesn\'t work',
detail: 'Printing is disabled due to an Electron upstream issue. Please export the document as PDF and print the PDF file. We apologize for the inconvenience!'
win.webContents.print({ printBackground: true }, () => {
removePrintServiceFromWindow(win)
})
// win.webContents.print({ printBackground: true }, () => {
// removePrintServiceFromWindow(win)
// })
}
const handleResponseForSave = async (e, { id, filename, markdown, pathname, options, defaultPath }) => {
@ -140,7 +130,7 @@ const handleResponseForSave = async (e, { id, filename, markdown, pathname, opti
return id
})
.catch(err => {
log.error(err)
log.error('Error while saving:', err)
win.webContents.send('mt::tab-save-failure', id, err.message)
})
}
@ -185,7 +175,7 @@ const openPandocFile = async (windowId, pathname) => {
const data = await converter()
ipcMain.emit('app-open-markdown-by-id', windowId, data)
} catch (err) {
log.error(err)
log.error('Error while converting file:', err)
}
}
@ -216,7 +206,7 @@ ipcMain.on('mt::save-and-close-tabs', async (e, unsavedFiles) => {
win.send('mt::force-close-tabs-by-id', tabIds)
})
.catch(err => {
log.error(err.error)
log.error('Error while save all:', err.error)
})
} else {
const tabIds = unsavedFiles.map(f => f.id)
@ -262,7 +252,7 @@ ipcMain.on('AGANI::response-file-save-as', async (e, { id, filename, markdown, p
}
})
.catch(err => {
log.error(err)
log.error('Error while save as:', err)
win.webContents.send('mt::tab-save-failure', id, err.message)
})
}
@ -282,8 +272,7 @@ ipcMain.on('mt::close-window-confirm', async (e, unsavedFiles) => {
ipcMain.emit('window-close-by-id', win.id)
})
.catch(err => {
console.log(err)
log.error(err)
log.error('Error while saving before quit:', err)
// Notify user about the problem.
dialog.showMessageBox(win, {
@ -446,19 +435,9 @@ export const importFile = async win => {
}
export const print = win => {
if (!win) {
return
if (win) {
win.webContents.send('mt::show-export-dialog', 'print')
}
// See GH#749, Electron#16085 and Electron#17523.
dialog.showMessageBox(win, {
type: 'info',
buttons: ['OK'],
defaultId: 0,
noLink: true,
message: 'Printing doesn\'t work',
detail: 'Printing is disabled due to an Electron upstream issue. Please export the document as PDF and print the PDF file. We apologize for the inconvenience!'
})
// win.webContents.send('mt::show-export-dialog', 'print')
}
export const openFile = async win => {

View File

@ -9,13 +9,13 @@ export const toggleAlwaysOnTop = win => {
export const zoomIn = win => {
const { webContents } = win
const zoom = webContents.getZoomFactor()
// WORKAROUND: Electron#16018
// WORKAROUND: We need to set zoom on the browser window due to Electron#16018.
webContents.send('mt::window-zoom', Math.min(2.0, zoom + 0.125))
}
export const zoomOut = win => {
const { webContents } = win
const zoom = webContents.getZoomFactor()
// WORKAROUND: Electron#16018
// WORKAROUND: We need to set zoom on the browser window due to Electron#16018.
webContents.send('mt::window-zoom', Math.max(1.0, zoom - 0.125))
}

View File

@ -92,7 +92,7 @@ class AppMenu {
}
return recentDocuments
} catch (err) {
log.error(err)
log.error('Error while read recently used documents:', err)
return []
}
}

View File

@ -166,7 +166,7 @@ export default function (keybindings) {
}
}, {
id: 'frontMatterMenuItem',
label: 'YAML Front Matter',
label: 'Front Matter',
type: 'checkbox',
accelerator: keybindings.getAccelerator('paragraphYAMLFrontMatter'),
click (menuItem, browserWindow) {

View File

@ -17,19 +17,13 @@ export default function (keybindings) {
toggleAlwaysOnTop(browserWindow)
}
}, {
// TODO: Disable due GH#1225.
visible: false,
type: 'separator'
}, {
// TODO: Disable due GH#1225.
visible: false,
label: 'Zoom In',
click (menuItem, browserWindow) {
zoomIn(browserWindow)
}
}, {
// TODO: Disable due GH#1225.
visible: false,
label: 'Zoom Out',
click (menuItem, browserWindow) {
zoomOut(browserWindow)

View File

@ -1,34 +0,0 @@
import { isWindows } from '../../config'
let GetStringRegKey = null
if (isWindows) {
try {
GetStringRegKey = require('vscode-windows-registry').GetStringRegKey
} catch (e) {
// Ignore webpack build error on macOS and Linux.
}
}
export const winHKEY = {
HKCU: 'HKEY_CURRENT_USER',
HKLM: 'HKEY_LOCAL_MACHINE',
HKCR: 'HKEY_CLASSES_ROOT',
HKU: 'HKEY_USERS',
HKCC: 'HKEY_CURRENT_CONFIG'
}
/**
* Returns the registry key value.
*
* @param {winHKEY} hive The registry key
* @param {string} path The registry subkey
* @param {string} name The registry name
* @returns {string|null|undefined} The registry key value or null/undefined.
*/
export const getStringRegKey = (hive, path, name) => {
try {
return GetStringRegKey(hive, path, name)
} catch (e) {
return null
}
}

View File

@ -3,24 +3,12 @@ import fs from 'fs'
import path from 'path'
import EventEmitter from 'events'
import Store from 'electron-store'
import { BrowserWindow, ipcMain, systemPreferences } from 'electron'
import { BrowserWindow, ipcMain, nativeTheme } from 'electron'
import log from 'electron-log'
import { isOsx, isWindows } from '../config'
import { isWindows } from '../config'
import { hasSameKeys } from '../utils'
import { getStringRegKey, winHKEY } from '../platform/win32/registry.js'
import schema from './schema'
const isDarkSystemMode = () => {
if (isOsx) {
return systemPreferences.isDarkMode()
} else if (isWindows) {
// NOTE: This key is a 32-Bit DWORD but converted to JS string!
const buf = getStringRegKey(winHKEY.HKCU, 'Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize', 'AppsUseLightTheme')
return buf === '' // zero (0)
}
return false
}
const PREFERENCES_FILE_NAME = 'preferences'
class Preference extends EventEmitter {
@ -50,7 +38,9 @@ class Preference extends EventEmitter {
let defaultSettings = null
try {
defaultSettings = fse.readJsonSync(this.staticPath)
if (isDarkSystemMode()) {
// Set best theme on first application start.
if (nativeTheme.shouldUseDarkColors) {
defaultSettings.theme = 'dark'
}
} catch (err) {

View File

@ -242,11 +242,25 @@
"type": "boolean",
"default": false
},
"footnote": {
"description": "Markdown-Enable pandoc's markdown extension footnote.",
"type": "boolean",
"default": false
},
"theme": {
"description": "Theme--Select the theme used in Mark Text",
"type": "string"
},
"autoSwitchTheme": {
"description": "Theme--Automatically adjust application theme according system.",
"default": 2,
"enum": [
0,
1,
2
]
},
"spellcheckerEnabled": {
"description": "Spelling--Whether spell checking is enabled.",

View File

@ -46,8 +46,9 @@ const filesHandler = (files, directory, key) => {
const rebuild = (directory) => {
fs.readdir(directory, (err, files) => {
if (err) log.error(err)
else {
if (err) {
log.error('imagePathAutoComplement::rebuild:', err)
} else {
filesHandler(files, directory)
}
})

View File

@ -3,7 +3,7 @@ import { BrowserWindow, ipcMain } from 'electron'
import electronLocalshortcut from '@hfelix/electron-localshortcut'
import BaseWindow, { WindowLifecycle, WindowType } from './base'
import { centerWindowOptions } from './utils'
import { TITLE_BAR_HEIGHT, defaultPreferenceWinOptions, isLinux, isOsx } from '../config'
import { TITLE_BAR_HEIGHT, preferencesWinOptions, isLinux, isOsx, isWindows } from '../config'
class SettingWindow extends BaseWindow {
/**
@ -21,12 +21,18 @@ class SettingWindow extends BaseWindow {
*/
createWindow (options = {}) {
const { menu: appMenu, env, keybindings, preferences } = this._accessor
const winOptions = Object.assign({}, defaultPreferenceWinOptions, options)
const winOptions = Object.assign({}, preferencesWinOptions, options)
centerWindowOptions(winOptions)
if (isLinux) {
winOptions.icon = path.join(__static, 'logo-96px.png')
}
// WORKAROUND: Electron has issues with different DPI per monitor when
// setting a fixed window size.
if (isWindows) {
winOptions.resizable = true
}
// Enable native or custom/frameless window and titlebar
const { titleBarStyle, theme } = preferences.getAll()
if (!isOsx) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -0,0 +1,15 @@
.footnotes {
font-size: .85em;
opacity: .8;
}
.footnotes li[role="doc-endnote"] {
position: relative;
}
.footnotes .footnote-back {
position: absolute;
font-family: initial;
top: .2em;
right: 1em;
}

View File

@ -180,6 +180,55 @@ figure[data-role="HTML"].ag-active .ag-html-preview {
display: none;
}
figure[data-role="FOOTNOTE"] {
position: relative;
background: var(--footnoteBgColor);
padding: 1.2em 2em .05em 1em;
font-size: .8em;
opacity: .8;
}
figure[data-role="FOOTNOTE"] > p:first-of-type .ag-paragraph-content:empty::after {
content: 'Input the footnote definition...';
color: var(--editorColor30);
}
figure[data-role="FOOTNOTE"].ag-active::before {
content: attr(data-role);
text-transform: lowercase;
position: absolute;
top: .2em;
right: 1em;
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: 0.2em;
left: 0;
font-size: 14px;
font-family: monospace;
font-weight: 600;
color: var(--editorColor);
background: transparent;
z-index: 1;
}
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;
@ -1194,3 +1243,28 @@ figure:not(.ag-active) pre.ag-paragraph.line-numbers {
top: .05em;
}
.ag-inline-footnote-identifier {
background: var(--codeBlockBgColor);
padding: 0 0.4em;
border-radius: 3px;
font-size: .7em;
color: var(--editorColor80);
}
.ag-inline-footnote-identifier a {
color: var(--editorColor);
}
i.ag-footnote-backlink {
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
display: block;
position: absolute;
right: .5em;
bottom: .5em;
font-family: sans-serif;
cursor: pointer;
z-index: 100;
}

View File

@ -107,6 +107,7 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_INLINE_IMAGE_SELECTED',
'AG_INLINE_IMAGE_IS_EDIT',
'AG_INDENT_CODE',
'AG_INLINE_FOOTNOTE_IDENTIFIER',
'AG_INLINE_RULE',
'AG_LANGUAGE',
'AG_LANGUAGE_INPUT',
@ -276,7 +277,8 @@ export const MUYA_DEFAULT_OPTION = {
imagePathAutoComplete: () => [],
// Markdown extensions
superSubScript: false
superSubScript: false,
footnote: false
}
// export const DIAGRAM_TEMPLATE = {

View File

@ -345,6 +345,29 @@ const backspaceCtrl = ContentState => {
}
if (
block.type === 'span' &&
block.functionType === 'paragraphContent' &&
left === 0 &&
preBlock &&
preBlock.functionType === 'footnoteInput'
) {
event.preventDefault()
event.stopPropagation()
if (!parent.nextSibling) {
const pBlock = this.createBlockP(block.text)
const figureBlock = this.closest(block, 'figure')
this.insertBefore(pBlock, figureBlock)
this.removeBlock(figureBlock)
const key = pBlock.children[0].key
const offset = 0
this.cursor = {
start: { key, offset },
end: { key, offset }
}
this.partialRender()
}
} else if (
block.type === 'span' &&
block.functionType === 'codeContent' &&
left === 0 &&
@ -492,7 +515,7 @@ const backspaceCtrl = ContentState => {
// also need to remove the paragrah
if (this.isOnlyChild(block) && block.type === 'span') {
this.removeBlock(parent)
} else if (block.functionType !== 'languageInput') {
} else if (block.functionType !== 'languageInput' && block.functionType !== 'footnoteInput') {
this.removeBlock(block)
}
@ -500,10 +523,14 @@ const backspaceCtrl = ContentState => {
start: { key, offset },
end: { key, offset }
}
if (this.isCollapse()) {
let needRenderAll = false
if (this.isCollapse() && preBlock.type === 'span' && preBlock.functionType === 'paragraphContent') {
this.checkInlineUpdate(preBlock)
needRenderAll = true
}
this.partialRender()
needRenderAll ? this.render() : this.partialRender()
}
}
}

View File

@ -191,7 +191,7 @@ const copyCutCtrl = ContentState => {
}
let htmlData = wrapper.innerHTML
const textData = this.htmlToMarkdown(htmlData)
const textData = escapeHtml(this.htmlToMarkdown(htmlData))
htmlData = marked(textData)
return { html: htmlData, text: textData }

View File

@ -1,6 +1,10 @@
import selection from '../selection'
import { isOsx } from '../config'
/* eslint-disable no-useless-escape */
const FOOTNOTE_REG = /^\[\^([^\^\[\]\s]+?)(?<!\\)\]:$/
/* eslint-enable no-useless-escape */
const checkAutoIndent = (text, offset) => {
const pairStr = text.substring(offset - 1, offset + 1)
return /^(\{\}|\[\]|\(\)|><)$/.test(pairStr)
@ -226,6 +230,26 @@ const enterCtrl = ContentState => {
return this.enterHandler(event)
}
if (
block.type === 'span' &&
block.functionType === 'paragraphContent' &&
!this.getParent(block).parent &&
start.offset === text.length &&
FOOTNOTE_REG.test(text)
) {
event.preventDefault()
event.stopPropagation()
// Just to feet the `updateFootnote` API and add one white space.
block.text += ' '
const key = block.key
const offset = block.text.length
this.cursor = {
start: { key, offset },
end: { key, offset }
}
return this.updateFootnote(this.getParent(block), block)
}
// handle `shift + enter` insert `soft line break` or `hard line break`
// only cursor in `line block` can create `soft line break` and `hard line break`
// handle line in code block
@ -418,6 +442,7 @@ const enterCtrl = ContentState => {
}
this.insertAfter(newBlock, block)
break
}
case left === 0 && right === 0: {
@ -511,7 +536,14 @@ const enterCtrl = ContentState => {
end: { key, offset }
}
this.partialRender()
let needRenderAll = false
if (this.isCollapse() && cursorBlock.type === 'p') {
this.checkInlineUpdate(cursorBlock.children[0])
needRenderAll = true
}
needRenderAll ? this.render() : this.partialRender()
}
}

View File

@ -0,0 +1,63 @@
/* eslint-disable no-useless-escape */
const FOOTNOTE_REG = /^\[\^([^\^\[\]\s]+?)(?<!\\)\]: /
/* eslint-enable no-useless-escape */
const footnoteCtrl = ContentState => {
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])
}
this.render()
return sectionWrapper
}
ContentState.prototype.createFootnote = function (identifier) {
const { blocks } = this
const lastBlock = blocks[blocks.length - 1]
const newBlock = this.createBlockP(`[^${identifier}]: `)
this.insertAfter(newBlock, lastBlock)
const key = newBlock.children[0].key
const offset = newBlock.children[0].text.length
this.cursor = {
start: { key, offset },
end: { key, offset }
}
const sectionWrapper = this.updateFootnote(newBlock, newBlock.children[0])
const id = sectionWrapper.key
const footnoteEle = document.querySelector(`#${id}`)
if (footnoteEle) {
footnoteEle.scrollIntoView({ behavior: 'smooth' })
}
}
}
export default footnoteCtrl

View File

@ -86,7 +86,12 @@ const clearFormat = (token, { start, end }) => {
}
const addFormat = (type, block, { start, end }) => {
if (block.type === 'pre') return false
if (
block.type !== 'span' ||
(block.type === 'span' && !/paragraphContent|cellConntent|atxLine/.test(block.functionType))
) {
return false
}
switch (type) {
case 'em':
case 'del':

View File

@ -29,7 +29,7 @@ const imageCtrl = ContentState => {
// Only encode URLs but not local paths or data URLs
let imgUrl
if (!/data:image/.test(src)) {
imgUrl = encodeURI(src)
imgUrl = encodeURI(src).replace(/#/g, encodeURIComponent('#'))
} else {
imgUrl = src
}
@ -132,7 +132,7 @@ const imageCtrl = ContentState => {
}
imageText += ']('
if (src) {
imageText += encodeURI(src)
imageText += encodeURI(src).replace(/#/g, encodeURIComponent('#'))
}
if (title) {
imageText += ` "${title}"`
@ -177,11 +177,19 @@ const imageCtrl = ContentState => {
this.selectedImage = imageInfo
const { key } = imageInfo
const block = this.getBlock(key)
const outMostBlock = this.findOutMostBlock(block)
this.cursor = {
start: { key, offset: imageInfo.token.range.end },
end: { key, offset: imageInfo.token.range.end }
}
return this.singleRender(block, true)
// Fix #1568
const { start } = this.prevCursor
const oldBlock = this.findOutMostBlock(this.getBlock(start.key))
if (oldBlock.key !== outMostBlock.key) {
this.singleRender(oldBlock, false)
}
return this.singleRender(outMostBlock, true)
}
}

View File

@ -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
]

View File

@ -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]+?(?<!\\\\)\\]: )', // Footnote **match from beginning**
'(?:^|\n) {0,3}((?:\\* *\\* *\\*|- *- *-|_ *_ *_)[ \\*\\-\\_]*)$' // Thematic break
]
@ -76,7 +77,7 @@ const updateCtrl = ContentState => {
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)

View File

@ -101,6 +101,7 @@ class ClickEvent {
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
const rubyText = rubyRender && rubyRender.previousElementSibling
@ -131,6 +132,20 @@ class ClickEvent {
return contentState.deleteImage(imageInfo)
}
if (footnoteBackLink) {
event.preventDefault()
event.stopPropagation()
const figure = event.target.closest('figure')
const identifier = figure.querySelector('span.ag-footnote-input').textContent
if (identifier) {
const footnoteIdentifier = document.querySelector(`#noteref-${identifier}`)
if (footnoteIdentifier) {
footnoteIdentifier.scrollIntoView({ behavior: 'smooth' })
}
}
return
}
// Handle image click, to select the current image
if (target.tagName === 'IMG' && imageWrapper) {
// Handle select image

View File

@ -1,4 +1,5 @@
import { getLinkInfo } from '../utils/getLinkInfo'
import { collectFootnotes } from '../utils'
class MouseEvent {
constructor (muya) {
@ -12,30 +13,69 @@ class MouseEvent {
const handler = event => {
const target = event.target
const parent = target.parentNode
const { hideLinkPopup } = this.muya.options
if (!hideLinkPopup && parent && parent.tagName === 'A' && parent.classList.contains('ag-inline-rule')) {
const rect = parent.getBoundingClientRect()
const reference = {
getBoundingClientRect () {
return rect
}
const preSibling = target.previousElementSibling
const parentPreSibling = parent ? parent.previousElementSibling : null
const { hideLinkPopup, footnote } = this.muya.options
const rect = parent.getBoundingClientRect()
const reference = {
getBoundingClientRect () {
return rect
}
}
if (
!hideLinkPopup &&
parent &&
parent.tagName === 'A' &&
parent.classList.contains('ag-inline-rule') &&
parentPreSibling &&
parentPreSibling.classList.contains('ag-hide')
) {
eventCenter.dispatch('muya-link-tools', {
reference,
linkInfo: getLinkInfo(parent)
})
}
if (
footnote &&
parent &&
parent.tagName === 'SUP' &&
parent.classList.contains('ag-inline-footnote-identifier') &&
preSibling &&
preSibling.classList.contains('ag-hide')
) {
const identifier = target.textContent
eventCenter.dispatch('muya-footnote-tool', {
reference,
identifier,
footnotes: collectFootnotes(this.muya.contentState.blocks)
})
}
}
const leaveHandler = event => {
const target = event.target
const parent = target.parentNode
const preSibling = target.previousElementSibling
const { footnote } = this.muya.options
if (parent && parent.tagName === 'A' && parent.classList.contains('ag-inline-rule')) {
eventCenter.dispatch('muya-link-tools', {
reference: null
})
}
if (
footnote &&
parent &&
parent.tagName === 'SUP' &&
parent.classList.contains('ag-inline-footnote-identifier') &&
preSibling &&
preSibling.classList.contains('ag-hide')
) {
eventCenter.dispatch('muya-footnote-tool', {
reference: null
})
}
}
eventCenter.attachDOMEvent(container, 'mouseover', handler)

View File

@ -32,12 +32,12 @@ const correctUrl = token => {
}
}
const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels, options) => {
const originSrc = src
const tokens = []
let pending = ''
let pendingStartPos = pos
const { superSubScript, footnote } = options
const pushPending = () => {
if (pending) {
tokens.push({
@ -151,7 +151,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
range,
marker,
parent: tokens,
children: tokenizerFac(to[2], undefined, inlineRules, pos + to[1].length, false, labels),
children: tokenizerFac(to[2], undefined, inlineRules, pos + to[1].length, false, labels, options),
backlash: to[3]
})
src = src.substring(to[0].length)
@ -192,7 +192,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
range,
marker,
parent: tokens,
children: tokenizerFac(to[2], undefined, inlineRules, pos + to[1].length, false, labels),
children: tokenizerFac(to[2], undefined, inlineRules, pos + to[1].length, false, labels, options),
backlash: to[3]
})
}
@ -203,7 +203,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
}
if (inChunk) continue
// superscript and subscript
if (inlineRules.superscript && inlineRules.subscript) {
if (superSubScript) {
const superSubTo = inlineRules.superscript.exec(src) || inlineRules.subscript.exec(src)
if (superSubTo) {
pushPending()
@ -223,6 +223,28 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
continue
}
}
// footnote identifier
if (pos !== 0 && footnote) {
const footnoteTo = inlineRules.footnote_identifier.exec(src)
if (footnoteTo) {
pushPending()
tokens.push({
type: 'footnote_identifier',
raw: footnoteTo[0],
marker: footnoteTo[1],
range: {
start: pos,
end: pos + footnoteTo[0].length
},
parent: tokens,
content: footnoteTo[2]
})
src = src.substring(footnoteTo[0].length)
pos = pos + footnoteTo[0].length
continue
}
}
// image
const imageTo = inlineRules.image.exec(src)
correctUrl(imageTo)
@ -276,7 +298,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
start: pos,
end: pos + linkTo[0].length
},
children: tokenizerFac(linkTo[2], undefined, inlineRules, pos + linkTo[1].length, false, labels),
children: tokenizerFac(linkTo[2], undefined, inlineRules, pos + linkTo[1].length, false, labels, options),
backlash: {
first: linkTo[3],
second: linkTo[5]
@ -306,7 +328,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
start: pos,
end: pos + rLinkTo[0].length
},
children: tokenizerFac(rLinkTo[1], undefined, inlineRules, pos + 1, false, labels)
children: tokenizerFac(rLinkTo[1], undefined, inlineRules, pos + 1, false, labels, options)
})
src = src.substring(rLinkTo[0].length)
@ -442,7 +464,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
parent: tokens,
attrs,
content: htmlTo[4],
children: htmlTo[4] ? tokenizerFac(htmlTo[4], undefined, inlineRules, pos + htmlTo[2].length, false, labels) : '',
children: htmlTo[4] ? tokenizerFac(htmlTo[4], undefined, inlineRules, pos + htmlTo[2].length, false, labels, options) : '',
range: {
start: pos,
end: pos + len
@ -530,16 +552,8 @@ export const tokenizer = (src, {
labels = new Map(),
options = {}
} = {}) => {
const { superSubScript } = options
if (superSubScript) {
inlineRules.superscript = inlineExtensionRules.superscript
inlineRules.subscript = inlineExtensionRules.subscript
} else {
delete inlineRules.superscript
delete inlineRules.subscript
}
const tokens = tokenizerFac(src, hasBeginRules ? beginRules : null, inlineRules, 0, true, labels)
const rules = Object.assign({}, inlineRules, inlineExtensionRules)
const tokens = tokenizerFac(src, hasBeginRules ? beginRules : null, rules, 0, true, labels, options)
const postTokenizer = tokens => {
for (const token of tokens) {

View File

@ -35,7 +35,8 @@ export const block = {
// extra
frontmatter: /^(?:(?:---\n([\s\S]+?)---)|(?:\+\+\+\n([\s\S]+?)\+\+\+)|(?:;;;\n([\s\S]+?);;;)|(?:\{\n([\s\S]+?)\}))(?:\n{2,}|\n{1,2}$)/,
multiplemath: /^\$\$\n([\s\S]+?)\n\$\$(?:\n+|$)/
multiplemath: /^\$\$\n([\s\S]+?)\n\$\$(?:\n+|$)/,
footnote: /^\[\^([^\^\[\]\s]+?)\]:[\s\S]+?(?=\n *\n {0,3}[^ ]+|$)/
}
block._label = /(?!\s*\])(?:\\[\[\]]|[^\[\]])+/

View File

@ -1,16 +1,17 @@
import Renderer from './renderer'
import { normal, breaks, gfm, pedantic } from './inlineRules'
import defaultOptions from './options'
import { escape, findClosingBracket } from './utils'
import { escape, findClosingBracket, getUniqueId } from './utils'
import { validateEmphasize, lowerPriority } from '../utils'
/**
* Inline Lexer & Compiler
*/
function InlineLexer (links, options) {
function InlineLexer (links, footnotes, options) {
this.options = options || defaultOptions
this.links = links
this.footnotes = footnotes
this.rules = normal
this.renderer = this.options.renderer || new Renderer()
this.renderer.options = this.options
@ -49,7 +50,7 @@ function InlineLexer (links, options) {
InlineLexer.prototype.output = function (src) {
// src = src
// .replace(/\u00a0/g, ' ')
const { disableInline, emoji, math, superSubScript } = this.options
const { disableInline, emoji, math, superSubScript, footnote } = this.options
if (disableInline) {
return escape(src)
}
@ -73,6 +74,19 @@ InlineLexer.prototype.output = function (src) {
continue
}
// footnote identifier
if (footnote) {
cap = this.rules.footnoteIdentifier.exec(src)
if (cap) {
src = src.substring(cap[0].length)
lastChar = cap[0].charAt(cap[0].length - 1)
const identifier = cap[1]
const footnoteInfo = this.footnotes[identifier] || {}
footnoteInfo.footnoteIdentifierId = getUniqueId()
out += this.renderer.footnoteIdentifier(identifier, footnoteInfo)
}
}
// tag
cap = this.rules.tag.exec(src)
if (cap) {

View File

@ -29,7 +29,7 @@ const inline = {
// ------------------------
// patched
// allow inline math "$" and superscript ("?=[\\<!\[`*]" to "?=[\\<!\[`*\$]")
// allow inline math "$" and superscript ("?=[\\<!\[`*]" to "?=[\\<!\[`*\$^]")
text: /^(`+|[^`])(?:[\s\S]*?(?:(?=[\\<!\[`*\$^]|\b_|$)|[^ ](?= {2,}\n))|(?= {2,}\n))/, // emoji is patched in gfm
// ------------------------
@ -41,7 +41,8 @@ const inline = {
// superscript and subScript
superscript: /^(\^)((?:[^\^\s]|(?<=\\)\1|(?<=\\) )+?)(?<!\\)\1(?!\1)/,
subscript: /^(~)((?:[^~\s]|(?<=\\)\1|(?<=\\) )+?)(?<!\\)\1(?!\1)/
subscript: /^(~)((?:[^~\s]|(?<=\\)\1|(?<=\\) )+?)(?<!\\)\1(?!\1)/,
footnoteIdentifier: /^\[\^([^\^\[\]\s]+?)(?<!\\)\]/
}
// list of punctuation marks from common mark spec
@ -114,7 +115,7 @@ export const gfm = Object.assign({}, normal, {
// ------------------------
// patched
// allow inline math "$" and emoji ":" and superscrpt "^" ("?=[\\<!\[`*~]|" to "?=[\\<!\[`*~:\$]|")
// allow inline math "$" and emoji ":" and superscrpt "^" ("?=[\\<!\[`*~]|" to "?=[\\<!\[`*~:\$^]|")
text: /^(`+|[^`])(?:[\s\S]*?(?:(?=[\\<!\[`*~:\$^]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@))|(?= {2,}\n|[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@))/,
// ------------------------

View File

@ -1,6 +1,6 @@
import { normal, gfm, pedantic } from './blockRules'
import options from './options'
import { splitCells, rtrim } from './utils'
import { splitCells, rtrim, getUniqueId } from './utils'
/**
* Block Lexer
@ -9,6 +9,8 @@ import { splitCells, rtrim } from './utils'
function Lexer (opts) {
this.tokens = []
this.tokens.links = Object.create(null)
this.tokens.footnotes = Object.create(null)
this.footnoteOrder = 0
this.options = Object.assign({}, options, opts)
this.rules = normal
@ -28,7 +30,32 @@ Lexer.prototype.lex = function (src) {
.replace(/\r\n|\r/g, '\n')
.replace(/\t/g, ' ')
this.checkFrontmatter = true
return this.token(src, true)
this.footnoteOrder = 0
this.token(src, true)
// Move footnote token to the end of tokens.
const { tokens } = this
const hasNoFootnoteTokens = []
const footnoteTokens = []
let isInFootnote = false
for (const token of tokens) {
const { type } = token
if (type === 'footnote_start') {
isInFootnote = true
footnoteTokens.push(token)
} else if (type === 'footnote_end') {
isInFootnote = false
footnoteTokens.push(token)
} else if (isInFootnote) {
footnoteTokens.push(token)
} else {
hasNoFootnoteTokens.push(token)
}
}
const result = [...hasNoFootnoteTokens, ...footnoteTokens]
result.links = tokens.links
result.footnotes = tokens.footnotes
return result
}
/**
@ -36,7 +63,7 @@ Lexer.prototype.lex = function (src) {
*/
Lexer.prototype.token = function (src, top) {
const { frontMatter, math } = this.options
const { frontMatter, math, footnote } = this.options
src = src.replace(/^ +$/gm, '')
let loose
@ -48,7 +75,6 @@ Lexer.prototype.token = function (src, top) {
let i
let tag
let l
let checked
// Only check front matter at the begining of a markdown file.
// Please see note in "blockquote" why we need "checkFrontmatter" and "top".
@ -128,6 +154,37 @@ Lexer.prototype.token = function (src, top) {
}
}
if (footnote) {
cap = this.rules.footnote.exec(src)
if (top && cap) {
src = src.substring(cap[0].length)
const identifier = cap[1]
this.tokens.push({
type: 'footnote_start',
identifier
})
this.tokens.footnotes[identifier] = {
order: ++this.footnoteOrder,
identifier,
footnoteId: getUniqueId()
}
/* eslint-disable no-useless-escape */
// Remove the footnote identifer prefix. eg: `[^identifier]: `.
cap = cap[0].replace(/^\[\^[^\^\[\]\s]+?(?<!\\)\]:\s+/gm, '')
// Remove the four whitespace before each block of footnote.
cap = cap.replace(/\n {4}(?=[^\s])/g, '\n')
/* eslint-enable no-useless-escape */
this.token(cap, top)
this.tokens.push({
type: 'footnote_end'
})
continue
}
}
// fences
cap = this.rules.fences.exec(src)
if (cap) {
@ -233,6 +290,7 @@ Lexer.prototype.token = function (src, top) {
// list
cap = this.rules.list.exec(src)
if (cap) {
let checked
src = src.substring(cap[0].length)
bull = cap[2]
let isOrdered = bull.length > 1
@ -367,7 +425,7 @@ Lexer.prototype.token = function (src, top) {
const isOrderedListItem = /\d/.test(bull)
this.tokens.push({
checked: checked,
checked,
listItemType: bull.length > 1 ? 'order' : (isTaskList ? 'task' : 'bullet'),
bulletMarkerOrDelimiter: isOrderedListItem ? bull.slice(-1) : bull.charAt(0),
type: loose ? 'loose_item_start' : 'list_item_start'
@ -534,8 +592,6 @@ Lexer.prototype.token = function (src, top) {
throw new Error('Infinite loop on byte: ' + src.charCodeAt(0))
}
}
return this.tokens
}
export default Lexer

View File

@ -28,5 +28,6 @@ export default {
emoji: true,
math: true,
frontMatter: true,
superSubScript: false
superSubScript: false,
footnote: false
}

View File

@ -11,6 +11,8 @@ import defaultOptions from './options'
function Parser (options) {
this.tokens = []
this.token = null
this.footnotes = null
this.footnoteIdentifier = ''
this.options = options || defaultOptions
this.options.renderer = this.options.renderer || new Renderer()
this.renderer = this.options.renderer
@ -23,14 +25,15 @@ function Parser (options) {
*/
Parser.prototype.parse = function (src) {
this.inline = new InlineLexer(src.links, this.options)
this.inline = new InlineLexer(src.links, src.footnotes, this.options)
// use an InlineLexer with a TextRenderer to extract pure text
this.inlineText = new InlineLexer(
src.links,
src.footnotes,
Object.assign({}, this.options, { renderer: new TextRenderer() })
)
this.tokens = src.reverse()
this.footnotes = src.footnotes
let out = ''
while (this.next()) {
out += this.tok()
@ -148,6 +151,27 @@ Parser.prototype.tok = function () {
return this.renderer.blockquote(body)
}
// All the tokens will be footnotes if it after a footnote_start token. Because we put all footnote token at the end.
case 'footnote_start': {
let body = ''
let itemBody = ''
this.footnoteIdentifier = this.token.identifier
while (this.next()) {
if (this.token.type === 'footnote_end') {
const footnoteInfo = this.footnotes[this.footnoteIdentifier]
body += this.renderer.footnoteItem(itemBody, footnoteInfo)
this.footnoteIdentifier = ''
itemBody = ''
} else if (this.token.type === 'footnote_start') {
this.footnoteIdentifier = this.token.identifier
itemBody = ''
} else {
itemBody += this.tok()
}
}
return this.renderer.footnote(body)
}
case 'list_start': {
let body = ''
let taskList = false

View File

@ -44,6 +44,18 @@ Renderer.prototype.script = function (content, marker) {
return `<${tagName}>${content}</${tagName}>`
}
Renderer.prototype.footnoteIdentifier = function (identifier, { footnoteId, footnoteIdentifierId, order }) {
return `<a href="#${footnoteId ? `fn${footnoteId}` : ''}" class="footnote-ref" id="fnref${footnoteIdentifierId}" role="doc-noteref"><sup>${order || identifier}</sup></a>`
}
Renderer.prototype.footnote = function (footnote) {
return '<section class="footnotes" role="doc-endnotes">\n<hr />\n<ol>\n' + footnote + '</ol>\n</section>\n'
}
Renderer.prototype.footnoteItem = function (content, { footnoteId, footnoteIdentifierId }) {
return `<li id="fn${footnoteId}" role="doc-endnote">${content}<a href="#${footnoteIdentifierId ? `fnref${footnoteIdentifierId}` : ''}" class="footnote-back" role="doc-backlink">↩︎</a></li>`
}
Renderer.prototype.code = function (code, infostring, escaped, codeBlockStyle) {
const lang = (infostring || '').match(/\S*/)[0]
if (this.options.highlight) {

View File

@ -2,6 +2,10 @@
* Helpers
*/
let uniqueIdCounter = 0
export const getUniqueId = () => ++uniqueIdCounter
export const escape = function escape (html, encode) {
if (encode) {
if (escape.escapeTest.test(html)) {

View File

@ -1,5 +1,6 @@
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'
@ -138,10 +139,12 @@ export default function renderContainerBlock (parent, block, activeBlocks, match
} else if (type === 'figure') {
if (functionType) {
Object.assign(data.dataset, { role: functionType.toUpperCase() })
if (functionType === 'table') {
if (functionType === 'table' && activeBlocks[0] && activeBlocks[0].functionType === 'cellContent') {
children.unshift(renderTableTools(activeBlocks))
} else {
} else if (functionType !== 'footnote') {
children.unshift(renderEditIcon())
} else {
children.push(footnoteJumpIcon())
}
}

View File

@ -0,0 +1,5 @@
import { h } from '../snabbdom'
export const footnoteJumpIcon = () => {
return h('i.ag-footnote-backlink', '↩︎')
}

View File

@ -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) {

View File

@ -101,7 +101,8 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
functionType !== 'codeContent' &&
functionType !== 'languageInput'
) {
const hasBeginRules = type === 'span'
const hasBeginRules = /paragraphContent|atxLine/.test(functionType)
tokens = tokenizer(text, {
highlights,
hasBeginRules,
@ -247,6 +248,8 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
} else if (type === 'span' && functionType === 'languageInput') {
const html = getHighlightHtml(text, highlights)
children = htmlToVNode(html)
} else if (type === 'span' && functionType === 'footnoteInput') {
Object.assign(data.attrs, { spellcheck: 'false' })
}
if (!block.parent) {

View File

@ -0,0 +1,23 @@
import { CLASS_OR_ID } from '../../../config'
export default function footnoteIdentifier (h, cursor, block, token, outerClass) {
const className = this.getClassName(outerClass, block, token, cursor)
const { marker } = token
const { start, end } = token.range
const startMarker = this.highlight(h, block, start, start + marker.length, token)
const endMarker = this.highlight(h, block, end - 1, end, token)
const content = this.highlight(h, block, start + marker.length, end - 1, token)
return [
h(`sup#noteref-${token.content}.${CLASS_OR_ID.AG_INLINE_FOOTNOTE_IDENTIFIER}.${CLASS_OR_ID.AG_INLINE_RULE}`, [
h(`span.${className}.${CLASS_OR_ID.AG_REMOVE}`, startMarker),
h('a', {
attrs: {
spellcheck: 'false'
}
}, content),
h(`span.${className}.${CLASS_OR_ID.AG_REMOVE}`, endMarker)
])
]
}

View File

@ -28,6 +28,7 @@ import htmlRuby from './htmlRuby'
import referenceLink from './referenceLink'
import referenceImage from './referenceImage'
import superSubScript from './superSubScript'
import footnoteIdentifier from './footnoteIdentifier'
export default {
backlashInToken,
@ -59,5 +60,6 @@ export default {
htmlRuby,
referenceLink,
referenceImage,
superSubScript
superSubScript,
footnoteIdentifier
}

View File

@ -41,6 +41,7 @@ export const inlineRules = {
export const inlineExtensionRules = {
// This is not the best regexp, because it not support `2^2\\^`.
superscript: /^(\^)((?:[^\^\s]|(?<=\\)\1|(?<=\\) )+?)(?<!\\)\1(?!\1)/,
subscript: /^(~)((?:[^~\s]|(?<=\\)\1|(?<=\\) )+?)(?<!\\)\1(?!\1)/
subscript: /^(~)((?:[^~\s]|(?<=\\)\1|(?<=\\) )+?)(?<!\\)\1(?!\1)/,
footnote_identifier: /^(\[\^)([^\^\[\]\s]+?)(?<!\\)\]/
}
/* eslint-enable no-useless-escape */

View File

@ -0,0 +1,53 @@
.ag-footnote-tool-container {
width: 300px;
border-radius: 5px;
}
.ag-footnote-tool-container .ag-footnote-tool > div {
display: flex;
height: 35px;
align-items: center;
color: var(--editorColor);
font-size: 12px;
padding: 0 10px;
}
.ag-footnote-tool .text {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex: 1;
}
.ag-footnote-tool .btn {
width: 40px;
display: inline-block;
cursor: pointer;
}
.ag-footnote-tool .icon-wrapper {
width: 14px;
height: 14px;
margin-right: 5px;
position: relative;
}
.ag-footnote-tool .icon-wrapper i.icon {
display: inline-block;
position: absolute;
top: 0;
height: 100%;
width: 100%;
overflow: hidden;
color: var(--iconColor);
transition: all .25s ease-in-out;
}
.ag-footnote-tool .icon-wrapper i.icon > i[class^=icon-] {
display: inline-block;
width: 100%;
height: 100%;
filter: drop-shadow(14px 0 currentColor);
position: relative;
left: -14px;
}

View File

@ -0,0 +1,148 @@
import BaseFloat from '../baseFloat'
import { patch, h } from '../../parser/render/snabbdom'
import WarningIcon from '../../assets/pngicon/warning/2.png'
import './index.css'
const getFootnoteText = block => {
let text = ''
const travel = block => {
if (block.children.length === 0 && block.text) {
text += block.text
} else if (block.children.length) {
for (const b of block.children) {
travel(b)
}
}
}
const blocks = block.children.slice(1)
for (const b of blocks) {
travel(b)
}
return text
}
const defaultOptions = {
placement: 'bottom',
modifiers: {
offset: {
offset: '0, 5'
}
},
showArrow: false
}
class LinkTools extends BaseFloat {
static pluginName = 'footnoteTool'
constructor (muya, options = {}) {
const name = 'ag-footnote-tool'
const opts = Object.assign({}, defaultOptions, options)
super(muya, name, opts)
this.oldVnode = null
this.identifier = null
this.footnotes = null
this.options = opts
this.hideTimer = null
const toolContainer = this.toolContainer = document.createElement('div')
this.container.appendChild(toolContainer)
this.floatBox.classList.add('ag-footnote-tool-container')
this.listen()
}
listen () {
const { eventCenter } = this.muya
super.listen()
eventCenter.subscribe('muya-footnote-tool', ({ reference, identifier, footnotes }) => {
if (reference) {
this.footnotes = footnotes
this.identifier = identifier
setTimeout(() => {
this.show(reference)
this.render()
}, 0)
} else {
if (this.hideTimer) {
clearTimeout(this.hideTimer)
}
this.hideTimer = setTimeout(() => {
this.hide()
}, 500)
}
})
const mouseOverHandler = () => {
if (this.hideTimer) {
clearTimeout(this.hideTimer)
}
}
const mouseOutHandler = () => {
this.hide()
}
eventCenter.attachDOMEvent(this.container, 'mouseover', mouseOverHandler)
eventCenter.attachDOMEvent(this.container, 'mouseleave', mouseOutHandler)
}
render () {
const { oldVnode, toolContainer, identifier, footnotes } = this
const hasFootnote = footnotes.has(identifier)
const iconWrapperSelector = 'div.icon-wrapper'
const icon = h('i.icon', h('i.icon-inner', {
style: {
background: `url(${WarningIcon}) no-repeat`,
'background-size': '100%'
}
}, ''))
const iconWrapper = h(iconWrapperSelector, icon)
let text = 'Can\'t find footnote with syntax [^abc]:'
if (hasFootnote) {
const footnoteBlock = footnotes.get(identifier)
text = getFootnoteText(footnoteBlock)
if (!text) {
text = 'Input the footnote definition...'
}
}
const textNode = h('span.text', text)
const button = h('a.btn', {
on: {
click: event => {
this.buttonClick(event, hasFootnote)
}
}
}, hasFootnote ? 'Go to' : 'Create')
const children = [textNode, button]
if (!hasFootnote) {
children.unshift(iconWrapper)
}
const vnode = h('div', children)
if (oldVnode) {
patch(oldVnode, vnode)
} else {
patch(toolContainer, vnode)
}
this.oldVnode = vnode
}
buttonClick (event, hasFootnote) {
event.preventDefault()
event.stopPropagation()
const { identifier, footnotes } = this
if (hasFootnote) {
const block = footnotes.get(identifier)
const key = block.key
const ele = document.querySelector(`#${key}`)
ele.scrollIntoView({ behavior: 'smooth' })
} else {
this.muya.contentState.createFootnote(identifier)
}
return this.hide()
}
}
export default LinkTools

View File

@ -7,6 +7,7 @@ import imageIcon from '../../assets/pngicon/format_image/2.png'
import linkIcon from '../../assets/pngicon/format_link/2.png'
import strikeIcon from '../../assets/pngicon/format_strike/2.png'
import mathIcon from '../../assets/pngicon/format_math/2.png'
import highlightIcon from '../../assets/pngicon/highlight/2.png'
import clearIcon from '../../assets/pngicon/format_clear/2.png'
const COMMAND_KEY = isOsx ? '⌘' : '⌃'
@ -32,6 +33,11 @@ const icons = [
tooltip: 'Strikethrough',
shortcut: `${COMMAND_KEY}+D`,
icon: strikeIcon
}, {
type: 'mark',
tooltip: 'Highlight',
shortcut: `⇧+${COMMAND_KEY}+H`,
icon: highlightIcon
}, {
type: 'inline_code',
tooltip: 'Inline Code',

View File

@ -49,8 +49,8 @@
.ag-format-picker li.item .icon-wrapper {
display: flex;
width: 14px;
height: 14px;
width: 16px;
height: 16px;
}
.ag-format-picker li.item .icon-wrapper i.icon {
@ -67,9 +67,9 @@
display: inline-block;
width: 100%;
height: 100%;
filter: drop-shadow(14px 0 currentColor);
filter: drop-shadow(16px 0 currentColor);
position: relative;
left: -14px;
left: -16px;
}
.ag-format-picker li.item.active .icon-wrapper i.icon {

View File

@ -33,16 +33,16 @@
margin-left: 10px;
margin-right: 8px;
display: flex;
width: 14px;
height: 14px;
width: 16px;
height: 16px;
color: var(--iconColor);
}
.ag-front-menu li.item .icon-wrapper i.icon {
display: flex;
position: relative;
height: 14px;
width: 14px;
height: 16px;
width: 16px;
overflow: hidden;
color: var(--iconColor);
transition: all .25s ease-in-out;
@ -52,9 +52,9 @@
display: inline-block;
width: 100%;
height: 100%;
filter: drop-shadow(14px 0 currentColor);
filter: drop-shadow(16px 0 currentColor);
position: relative;
left: -14px;
left: -16px;
}
.ag-front-menu > ul li > span {

View File

@ -108,6 +108,7 @@
text-align: center;
display: block;
color: var(--editorColor30);
user-select: none;
}
.ag-image-selector span.description a {
@ -154,6 +155,7 @@
text-align: center;
font-size: 14px;
color: var(--editorColor);
user-select: none;
}
.ag-image-selector .more {
@ -161,6 +163,7 @@
color: var(--editorColor);
text-align: center;
margin-bottom: 20px;
user-select: none;
}
.ag-image-selector .photo {

View File

@ -12,7 +12,7 @@ class ImageSelector extends BaseFloat {
constructor (muya, options) {
const name = 'ag-image-selector'
const { accessKey } = options
const { unsplashAccessKey } = options
options = Object.assign(options, {
placement: 'bottom-center',
modifiers: {
@ -26,9 +26,13 @@ class ImageSelector extends BaseFloat {
this.renderArray = []
this.oldVnode = null
this.imageInfo = null
this.unsplash = new Unsplash({
accessKey
})
if (!unsplashAccessKey) {
this.unsplash = null
} else {
this.unsplash = new Unsplash({
accessKey: unsplashAccessKey
})
}
this.photoList = []
this.loading = false
this.tab = 'link' // select or link
@ -56,22 +60,27 @@ class ImageSelector extends BaseFloat {
}
Object.assign(this.state, imageInfo.token.attrs)
// load latest unsplash photos.
this.loading = true
this.unsplash.photos.listPhotos(1, 40, 'latest')
.then(toJson)
.then(json => {
this.loading = false
if (Array.isArray(json)) {
this.photoList = json
if (this.tab === 'unsplash') {
this.render()
if (this.unsplash) {
// Load latest unsplash photos.
this.loading = true
this.unsplash.photos.listPhotos(1, 40, 'latest')
.then(toJson)
.then(json => {
this.loading = false
if (Array.isArray(json)) {
this.photoList = json
if (this.tab === 'unsplash') {
this.render()
}
}
}
})
})
}
this.imageInfo = imageInfo
this.show(reference, cb)
this.render()
// Auto focus and select all content of the `src.input` element.
const input = this.imageSelectorContainer.querySelector('input.src')
if (input) {
@ -85,6 +94,10 @@ class ImageSelector extends BaseFloat {
}
searchPhotos = (keyword) => {
if (!this.unsplash) {
return
}
this.loading = true
this.photoList = []
this.unsplash.search.photos(keyword, 1, 40)
@ -253,11 +266,15 @@ class ImageSelector extends BaseFloat {
}, {
label: 'Embed link',
value: 'link'
}, {
label: 'Unsplash',
value: 'unsplash'
}]
if (this.unsplash) {
tabs.push({
label: 'Unsplash',
value: 'unsplash'
})
}
const children = tabs.map(tab => {
const itemSelector = this.tab === tab.value ? 'li.active' : 'li'
return h(itemSelector, h('span', {
@ -285,7 +302,7 @@ class ImageSelector extends BaseFloat {
}
}
}, 'Choose an Image'),
h('span.description', 'Choose image from you computer.')
h('span.description', 'Choose image from your computer.')
]
} else if (tab === 'link') {
const altInput = h('input.alt', {
@ -355,14 +372,14 @@ class ImageSelector extends BaseFloat {
}
}, 'Embed Image')
const bottomDes = h('span.description', [
h('span', 'Paste web image or local image path, '),
h('span', 'Paste web image or local image path. Use '),
h('a', {
on: {
click: event => {
this.toggleMode()
}
}
}, `${isFullMode ? 'simple mode' : 'full mode'}`)
}, `${isFullMode ? 'simple mode' : 'full mode'}.`)
])
bodyContent = [inputWrapper, embedButton, bottomDes]
} else {

View File

@ -3,6 +3,7 @@ import Prism from 'prismjs'
import katex from 'katex'
import loadRenderer from '../renderers'
import githubMarkdownCss from 'github-markdown-css/github-markdown.css'
import footnoteCss from '../assets/styles/exportStyle.css'
import highlightCss from 'prismjs/themes/prism.css'
import katexCss from 'katex/dist/katex.css'
import footerHeaderCss from '../assets/styles/headerFooterStyle.css'
@ -106,6 +107,7 @@ class ExportHtml {
this.mathRendererCalled = false
let html = marked(this.markdown, {
superSubScript: this.muya ? this.muya.options.superSubScript : false,
footnote: this.muya ? this.muya.options.footnote : false,
highlight (code, lang) {
// Language may be undefined (GH#591)
if (!lang) {
@ -247,6 +249,7 @@ class ExportHtml {
list-style-type: decimal;
}
</style>
<style>${footnoteCss}</style>
<style>${extraCss}</style>
</head>
<body>

View File

@ -4,8 +4,9 @@
* Before you edit or update codes in this file,
* make sure you have read this bellow:
* Commonmark Spec: https://spec.commonmark.org/0.29/
* and GitHub Flavored Markdown Spec: https://github.github.com/gfm/
* The output markdown needs to obey the standards of the two Spec.
* GitHub Flavored Markdown Spec: https://github.github.com/gfm/
* Pandoc Markdown: https://pandoc.org/MANUAL.html#pandocs-markdown
* The output markdown needs to obey the standards of these Spec.
*/
class ExportMarkdown {
@ -74,6 +75,10 @@ class ExportMarkdown {
result.push(this.normalizeHTML(block, indent))
break
}
case 'footnote': {
result.push(this.normalizeFootnote(block, indent))
break
}
case 'multiplemath': {
result.push(this.normalizeMultipleMath(block, indent))
break
@ -387,6 +392,24 @@ class ExportMarkdown {
result.push(this.translateBlocks2Markdown(children, newIndent, listIndent).substring(newIndent.length))
return result.join('')
}
normalizeFootnote (block, indent) {
const result = []
const identifier = block.children[0].text
result.push(`${indent}[^${identifier}]:`)
const hasMultipleBlocks = block.children.length > 2 || block.children[1].type !== 'p'
if (hasMultipleBlocks) {
result.push('\n')
const newIndent = indent + ' '.repeat(4)
result.push(this.translateBlocks2Markdown(block.children.slice(1), newIndent))
} else {
result.push(' ')
const paragraphContent = block.children[1].children[0]
result.push(this.normalizeParagraphText(paragraphContent, indent))
}
return result.join('')
}
}
export default ExportMarkdown

View File

@ -77,8 +77,9 @@ const importRegister = ContentState => {
nextSibling: null,
children: []
}
const { trimUnnecessaryCodeBlockEmptyLines } = this.muya.options
const tokens = new Lexer({ disableInline: true }).lex(markdown)
const { trimUnnecessaryCodeBlockEmptyLines, footnote } = this.muya.options
const tokens = new Lexer({ disableInline: true, footnote }).lex(markdown)
let token
let block
let value
@ -320,6 +321,23 @@ const importRegister = ContentState => {
parentList.shift()
break
}
case 'footnote_start': {
block = this.createBlock('figure', {
functionType: 'footnote'
})
const identifierInput = this.createBlock('span', {
text: token.identifier,
functionType: 'footnoteInput'
})
this.appendChild(block, identifierInput)
this.appendChild(parentList[0], block)
parentList.unshift(block)
break
}
case 'footnote_end': {
parentList.shift()
break
}
case 'list_start': {
const { ordered, listType, start } = token
block = this.createBlock(ordered === true ? 'ol' : 'ul')
@ -555,7 +573,6 @@ const importRegister = ContentState => {
results.add(attrs.src)
} else {
const rawSrc = label + backlash.second
console.log(render.labels)
if (render.labels.has((rawSrc).toLowerCase())) {
const { href } = render.labels.get(rawSrc.toLowerCase())
const { src } = getImageInfo(href)

View File

@ -387,3 +387,15 @@ export const verticalPositionInRect = (event, rect) => {
const { top, height } = rect
return (clientY - top) > (height / 2) ? 'down' : 'up'
}
export const collectFootnotes = (blocks) => {
const map = new Map()
for (const block of blocks) {
if (block.type === 'figure' && block.functionType === 'footnote') {
const identifier = block.children[0].text
map.set(identifier, block)
}
}
return map
}

View File

@ -640,7 +640,6 @@ kbd {
border-left-color: transparent;
border-right-color: transparent;
}
} /* end not print */
@media print {

View File

@ -32,6 +32,7 @@
--iconColor: #6B737B;
--codeBgColor: #d8d8d869;
--codeBlockBgColor: rgba(0, 0, 0, 0.03);
--footnoteBgColor: rgba(0, 0, 0, .03);
--inputBgColor: rgba(0, 0, 0, .06);
--focusColor: var(--themeColor);

View File

@ -26,6 +26,7 @@
--iconColor: rgba(255, 255, 255, .56);
--codeBgColor: #424344;
--codeBlockBgColor: #424344;
--footnoteBgColor: rgba(66, 67, 68, .3);
--inputBgColor: #2f3336;
--focusColor: var(--themeColor);

View File

@ -25,6 +25,7 @@
--iconColor: rgba(150, 150, 150, .8);
--codeBgColor: #d8d8d869;
--codeBlockBgColor: rgba(104, 134, 170, .05);
--footnoteBgColor: rgba(0, 0, 0, .03);
--inputBgColor: rgba(0, 0, 0, .06);
--focusColor: var(--themeColor);

View File

@ -26,6 +26,7 @@
--iconColor: rgba(255, 255, 255, .56);
--codeBgColor: #d8d8d869;
--codeBlockBgColor: #3f454c;
--footnoteBgColor: rgba(66, 67, 68, .5);
--inputBgColor: rgba(0, 0, 0, .1);
--focusColor: var(--themeColor);

View File

@ -27,6 +27,7 @@
--iconColor: rgba(255, 255, 255, .56);
--codeBgColor: #3a3f4b;
--codeBlockBgColor: #3a3f4b;
--footnoteBgColor: rgba(66, 67, 68, .5);
--inputBgColor: rgba(0, 0, 0, .1);
--focusColor: #568af2;

View File

@ -25,6 +25,7 @@
--iconColor: rgba(101, 101, 101, .8);
--codeBgColor: #d8d8d869;
--codeBlockBgColor: rgba(12, 139, 186, .05);
--footnoteBgColor: rgba(0, 0, 0, .03);
--inputBgColor: rgba(0, 0, 0, .06);
--focusColor: var(--themeColor);

View File

@ -88,6 +88,7 @@ import ImageToolbar from 'muya/lib/ui/imageToolbar'
import Transformer from 'muya/lib/ui/transformer'
import FormatPicker from 'muya/lib/ui/formatPicker'
import LinkTools from 'muya/lib/ui/linkTools'
import FootnoteTool from 'muya/lib/ui/footnoteTool'
import TableBarTools from 'muya/lib/ui/tableTools'
import FrontMenu from 'muya/lib/ui/frontMenu'
import Search from '../search'
@ -136,6 +137,7 @@ export default {
listIndentation: state => state.preferences.listIndentation,
frontmatterType: state => state.preferences.frontmatterType,
superSubScript: state => state.preferences.superSubScript,
footnote: state => state.preferences.footnote,
lineHeight: state => state.preferences.lineHeight,
fontSize: state => state.preferences.fontSize,
codeFontSize: state => state.preferences.codeFontSize,
@ -251,6 +253,12 @@ export default {
editor.setOptions({ superSubScript: value }, true)
}
},
footnote: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
editor.setOptions({ footnote: value }, true)
}
},
hideQuickInsertHint: function (value, oldValue) {
const { editor } = this
if (value !== oldValue && editor) {
@ -454,6 +462,7 @@ export default {
listIndentation,
frontmatterType,
superSubScript,
footnote,
hideQuickInsertHint,
editorLineWidth,
theme,
@ -468,7 +477,7 @@ export default {
Muya.use(EmojiPicker)
Muya.use(ImagePathPicker)
Muya.use(ImageSelector, {
accessKey: process.env.UNSPLASH_ACCESS_KEY,
unsplashAccessKey: process.env.UNSPLASH_ACCESS_KEY,
photoCreatorClick: this.photoCreatorClick
})
Muya.use(Transformer)
@ -478,6 +487,7 @@ export default {
Muya.use(LinkTools, {
jumpClick: this.jumpClick
})
Muya.use(FootnoteTool)
Muya.use(TableBarTools)
const options = {
@ -497,6 +507,7 @@ export default {
listIndentation,
frontmatterType,
superSubScript,
footnote,
hideQuickInsertHint,
hideLinkPopup,
spellcheckEnabled: spellcheckerEnabled,

View File

@ -1,6 +1,7 @@
import { remote } from 'electron'
import log from 'electron-log'
import bus from '@/bus'
import { getLanguageName } from '@/spellchecker/languageMap'
import { SEPARATOR } from './menuItems'
const { MenuItem } = remote
@ -24,7 +25,7 @@ export default (spellchecker, selectedWord, wordSuggestions, replaceCallback) =>
const availableDictionariesSubmenu = []
for (const dict of availableDictionaries) {
availableDictionariesSubmenu.push(new MenuItem({
label: dict,
label: getLanguageName(dict),
enabled: dict !== currentLanguage,
click () {
bus.$emit('switch-spellchecker-language', dict)

View File

@ -146,6 +146,7 @@ export default {
dispatch('LINTEN_FOR_PRINT_SERVICE_CLEARUP')
dispatch('LINTEN_FOR_EXPORT_SUCCESS')
dispatch('LISTEN_FOR_FILE_CHANGE')
dispatch('LISTEN_WINDOW_ZOOM')
// module: notification
dispatch('LISTEN_FOR_NOTIFICATION')

View File

@ -54,6 +54,12 @@
:onChange="value => onSelectChange('superSubScript', value)"
more="https://pandoc.org/MANUAL.html#superscripts-and-subscripts"
></bool>
<bool
description="Enable pandoc's markdown extension footnote(need restart Mark Text)."
:bool="footnote"
:onChange="value => onSelectChange('footnote', value)"
more="https://pandoc.org/MANUAL.html#footnotes"
></bool>
</div>
</template>
@ -95,7 +101,8 @@ export default {
tabSize: state => state.preferences.tabSize,
listIndentation: state => state.preferences.listIndentation,
frontmatterType: state => state.preferences.frontmatterType,
superSubScript: state => state.preferences.superSubScript
superSubScript: state => state.preferences.superSubScript,
footnote: state => state.preferences.footnote
})
},
methods: {

View File

@ -8,7 +8,7 @@
></bool>
<separator></separator>
<bool
description="When enabled Hunspell is used instead the OS spell checker (macOS only). The change take effect after application restart or for new editor windows."
description="When enabled, Hunspell is used instead the OS spell checker (macOS only). The change take effect after application restart or for new editor windows."
:bool="spellcheckerIsHunspell"
:disable="!isOsx || !spellcheckerEnabled"
:onChange="value => onSelectChange('spellcheckerIsHunspell', value)"
@ -20,7 +20,7 @@
:onChange="value => onSelectChange('spellcheckerNoUnderline', value)"
></bool>
<bool
description="Try to automatically identify the used language when typing. This feature is currently not available for Hunspell or when spelling mistakes are not underlined."
description="Try to automatically identify the used language as you type. This feature is currently not available for Hunspell or when spelling mistakes are not underlined."
:bool="spellcheckerAutoDetectLanguage"
:disable="!spellcheckerEnabled"
:onChange="value => onSelectChange('spellcheckerAutoDetectLanguage', value)"
@ -40,7 +40,7 @@
</div>
<div v-if="isHunspellSelected && spellcheckerEnabled">
<separator></separator>
<div class="description">Available Hunspell dictionaries. Please add additional language dictionaries via button below.</div>
<div class="description">List of available Hunspell dictionaries. Please add additional language dictionaries via drop-down menu below.</div>
<el-table
:data="availableDictionaries"
style="width: 100%">
@ -65,7 +65,7 @@
</el-table-column>
</el-table>
<div class="description">Add new dictionaries to Hunspell.</div>
<div class="description">Download new dictionaries for Hunspell.</div>
<div class="dictionary-group">
<el-select
v-model="selectedDictionaryToAdd"
@ -210,8 +210,13 @@ export default {
},
startDownloadHunspellDictionary (languageCode) {
this.errorMessage = ''
if (this.hunspellDictionaryDownloadCache[languageCode]) {
return
} else if (!navigator.onLine) {
delete this.hunspellDictionaryDownloadCache[languageCode]
this.errorMessage = 'No Internet connection available.'
return
}
this.hunspellDictionaryDownloadCache[languageCode] = 1

View File

@ -18,3 +18,14 @@ export const themes = [
name: 'one-dark'
}
]
export const autoSwitchThemeOptions = [{
label: 'Adjust theme at startup', // Always
value: 0
}, /* {
label: 'Only at runtime',
value: 1
}, */ {
label: 'Never',
value: 2
}]

View File

@ -4,12 +4,19 @@
<section class="offcial-themes">
<div v-for="t of themes" :key="t.name" class="theme"
:class="[t.name, { 'active': t.name === theme }]"
@click="handleSelectTheme(t.name)"
@click="onSelectChange('theme', t.name)"
>
<div v-html="t.html"></div>
</div>
</section>
<separator></separator>
<cur-select
description="Automatically adjust application theme according system."
:value="autoSwitchTheme"
:options="autoSwitchThemeOptions"
:onChange="value => onSelectChange('autoSwitchTheme', value)"
></cur-select>
<separator></separator>
<section class="import-themes ag-underdevelop">
<div>
<span>Open the themes folder</span>
@ -27,21 +34,25 @@
<script>
import { mapState } from 'vuex'
import themeMd from './theme.md'
import { themes } from './config'
import { autoSwitchThemeOptions, themes } from './config'
import markdownToHtml from '@/util/markdownToHtml'
import CurSelect from '../common/select'
import Separator from '../common/separator'
export default {
components: {
CurSelect,
Separator
},
data () {
this.autoSwitchThemeOptions = autoSwitchThemeOptions
return {
themes: []
}
},
computed: {
...mapState({
autoSwitchTheme: state => state.preferences.autoSwitchTheme,
theme: state => state.preferences.theme
})
},
@ -60,11 +71,8 @@ export default {
})
},
methods: {
handleSelectTheme (theme) {
this.$store.dispatch('SET_SINGLE_PREFERENCE', {
type: 'theme',
value: theme
})
onSelectChange (type, value) {
this.$store.dispatch('SET_SINGLE_PREFERENCE', { type, value })
}
}
}
@ -84,7 +92,7 @@ export default {
cursor: pointer;
width: 250px;
height: 100px;
margin: 0px 22px 10px 22px;
margin: 0px 20px 10px 20px;
padding-left: 30px;
padding-top: 20px;
overflow: hidden;

View File

@ -17,11 +17,26 @@ export const downloadHunspellDictionary = async lang => {
responseType: 'stream'
})
const dstFile = path.join(dictionaryPath, `${lang}.bdic`)
const tmpFile = `${dstFile}.tmp`
return new Promise((resolve, reject) => {
const outStream = fs.createWriteStream(path.join(dictionaryPath, `${lang}.bdic`))
const outStream = fs.createWriteStream(tmpFile)
response.data.pipe(outStream)
let totalLength = 0
response.data.on('data', chunk => {
totalLength += chunk.length
})
outStream.once('error', reject)
outStream.once('finish', () => resolve())
outStream.once('finish', async () => {
if (totalLength < 8 * 1024) {
throw new Error('Dictionary is most likely bogus.')
}
await fs.move(tmpFile, dstFile, { overwrite: true })
resolve()
})
})
}

View File

@ -320,21 +320,21 @@ export class SpellChecker {
/**
* Returns true if not misspelled words should be highlighted.
*/
get spellcheckerNoUnderline () {
get isPassiveMode () {
if (!this.isEnabled) {
return false
}
return this.provider.spellcheckerNoUnderline
return this.provider.isPassiveMode
}
/**
* Should we highlight misspelled words.
*/
set spellcheckerNoUnderline (value) {
set isPassiveMode (value) {
if (!this.isEnabled) {
return
}
this.provider.spellcheckerNoUnderline = !!value
this.provider.isPassiveMode = !!value
}
/**

View File

@ -42,8 +42,10 @@ const state = {
listIndentation: 1,
frontmatterType: '-',
superSubScript: false,
footnote: false,
theme: 'light',
autoSwitchTheme: 2,
spellcheckerEnabled: false,
spellcheckerIsHunspell: false, // macOS only

View File

@ -39,8 +39,10 @@
"listIndentation": 1,
"frontmatterType": "-",
"superSubScript": false,
"footnote": false,
"theme": "light",
"autoSwitchTheme": 2,
"spellcheckerEnabled": false,
"spellcheckerIsHunspell": false,

1429
yarn.lock

File diff suppressed because it is too large Load Diff