fix conflict
@ -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 } },
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
13
package.json
@ -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"
|
||||
|
@ -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'
|
||||
|
@ -16,7 +16,7 @@ export const editorWinOptions = {
|
||||
zoomFactor: 1.0
|
||||
}
|
||||
|
||||
export const defaultPreferenceWinOptions = {
|
||||
export const preferencesWinOptions = {
|
||||
width: 950,
|
||||
height: 650,
|
||||
webPreferences: {
|
||||
|
@ -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)
|
||||
|
@ -235,7 +235,7 @@ class Watcher {
|
||||
})
|
||||
}
|
||||
} else {
|
||||
log.error(error)
|
||||
log.error('Error while watching files:', error)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 => {
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ class AppMenu {
|
||||
}
|
||||
return recentDocuments
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
log.error('Error while read recently used documents:', err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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.",
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
@ -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) {
|
||||
|
BIN
src/muya/lib/assets/pngicon/footnote/1.png
Executable file
After Width: | Height: | Size: 500 B |
BIN
src/muya/lib/assets/pngicon/footnote/2.png
Executable file
After Width: | Height: | Size: 857 B |
BIN
src/muya/lib/assets/pngicon/footnote/3.png
Executable file
After Width: | Height: | Size: 1.2 KiB |
BIN
src/muya/lib/assets/pngicon/highlight/1.png
Executable file
After Width: | Height: | Size: 795 B |
BIN
src/muya/lib/assets/pngicon/highlight/2.png
Executable file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/muya/lib/assets/pngicon/highlight/3.png
Executable file
After Width: | Height: | Size: 2.1 KiB |
BIN
src/muya/lib/assets/pngicon/warning/2.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
15
src/muya/lib/assets/styles/exportStyle.css
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
63
src/muya/lib/contentState/footnoteCtrl.js
Normal 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
|
@ -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':
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
]
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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*\])(?:\\[\[\]]|[^\[\]])+/
|
||||
|
@ -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) {
|
||||
|
@ -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.!#$%&'*+\/=?_`{\|}~-]+@))/,
|
||||
|
||||
// ------------------------
|
||||
|
@ -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
|
||||
|
@ -28,5 +28,6 @@ export default {
|
||||
emoji: true,
|
||||
math: true,
|
||||
frontMatter: true,
|
||||
superSubScript: false
|
||||
superSubScript: false,
|
||||
footnote: false
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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)) {
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
import { h } from '../snabbdom'
|
||||
|
||||
export const footnoteJumpIcon = () => {
|
||||
return h('i.ag-footnote-backlink', '↩︎')
|
||||
}
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
])
|
||||
]
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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 */
|
||||
|
53
src/muya/lib/ui/footnoteTool/index.css
Normal 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;
|
||||
}
|
148
src/muya/lib/ui/footnoteTool/index.js
Normal 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
|
@ -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',
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -640,7 +640,6 @@ kbd {
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
} /* end not print */
|
||||
|
||||
@media print {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}]
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -42,8 +42,10 @@ const state = {
|
||||
listIndentation: 1,
|
||||
frontmatterType: '-',
|
||||
superSubScript: false,
|
||||
footnote: false,
|
||||
|
||||
theme: 'light',
|
||||
autoSwitchTheme: 2,
|
||||
|
||||
spellcheckerEnabled: false,
|
||||
spellcheckerIsHunspell: false, // macOS only
|
||||
|
@ -39,8 +39,10 @@
|
||||
"listIndentation": 1,
|
||||
"frontmatterType": "-",
|
||||
"superSubScript": false,
|
||||
"footnote": false,
|
||||
|
||||
"theme": "light",
|
||||
"autoSwitchTheme": 2,
|
||||
|
||||
"spellcheckerEnabled": false,
|
||||
"spellcheckerIsHunspell": false,
|
||||
|