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: [
|
use: [
|
||||||
'to-string-loader',
|
'to-string-loader',
|
||||||
'css-loader'
|
'css-loader'
|
||||||
@ -59,7 +59,7 @@ const rendererConfig = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
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: [
|
use: [
|
||||||
proMode ? MiniCssExtractPlugin.loader : 'style-loader',
|
proMode ? MiniCssExtractPlugin.loader : 'style-loader',
|
||||||
{ loader: 'css-loader', options: { importLoaders: 1 } },
|
{ 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 |
|
| 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) |
|
| frontmatterType | String | `-` | The frontmatter type: `-` (YAML), `+` (TOML), `;` (JSON) or `{` (JSON) |
|
||||||
| superSubScript | Boolean | `false` | Enable pandoc's markdown extension superscript and subscript. |
|
| superSubScript | Boolean | `false` | Enable pandoc's markdown extension superscript and subscript. |
|
||||||
|
| footnote | Boolean | `false` | Enable pandoc's footnote markdown extension |
|
||||||
|
|
||||||
#### Theme
|
#### Theme
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
- languageInput
|
- languageInput
|
||||||
|
|
||||||
|
- footnoteInput
|
||||||
|
|
||||||
- codeContent (used in code block)
|
- codeContent (used in code block)
|
||||||
|
|
||||||
- cellContent (used in table cell, it's parent must be th or td 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
|
- table
|
||||||
|
|
||||||
|
- footnote
|
||||||
|
|
||||||
- html
|
- html
|
||||||
|
|
||||||
- multiplemath
|
- multiplemath
|
||||||
|
@ -19,6 +19,7 @@ files:
|
|||||||
- "!node_modules/vega-lite/build/vega-lite*.js.map"
|
- "!node_modules/vega-lite/build/vega-lite*.js.map"
|
||||||
# Don't bundle build files
|
# Don't bundle build files
|
||||||
- "!node_modules/@felixrieseberg/spellchecker/bin"
|
- "!node_modules/@felixrieseberg/spellchecker/bin"
|
||||||
|
- "!node_modules/@hfelix/spellchecker/bin"
|
||||||
- "!node_modules/ced/bin"
|
- "!node_modules/ced/bin"
|
||||||
- "!node_modules/ced/vendor"
|
- "!node_modules/ced/vendor"
|
||||||
- "!node_modules/cld/bin"
|
- "!node_modules/cld/bin"
|
||||||
@ -34,6 +35,7 @@ files:
|
|||||||
- "!node_modules/ced/build/vendor"
|
- "!node_modules/ced/build/vendor"
|
||||||
# Don't bundle LGPL source files
|
# Don't bundle LGPL source files
|
||||||
- "!node_modules/@felixrieseberg/spellchecker/vendor"
|
- "!node_modules/@felixrieseberg/spellchecker/vendor"
|
||||||
|
- "!node_modules/@hfelix/spellchecker/vendor"
|
||||||
extraFiles:
|
extraFiles:
|
||||||
- "LICENSE"
|
- "LICENSE"
|
||||||
- from: "resources/THIRD-PARTY-LICENSES.txt"
|
- from: "resources/THIRD-PARTY-LICENSES.txt"
|
||||||
|
13
package.json
@ -34,7 +34,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hfelix/electron-localshortcut": "^3.1.1",
|
"@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",
|
"@octokit/rest": "^16.33.1",
|
||||||
"arg": "^4.1.1",
|
"arg": "^4.1.1",
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
@ -65,7 +65,7 @@
|
|||||||
"joplin-turndown-plugin-gfm": "^1.0.11",
|
"joplin-turndown-plugin-gfm": "^1.0.11",
|
||||||
"katex": "^0.11.1",
|
"katex": "^0.11.1",
|
||||||
"keyboard-layout": "^2.0.16",
|
"keyboard-layout": "^2.0.16",
|
||||||
"keytar": "^5.0.0-beta.3",
|
"keytar": "5.0.0-beta.4",
|
||||||
"mermaid": "^8.4.0",
|
"mermaid": "^8.4.0",
|
||||||
"plist": "^3.0.1",
|
"plist": "^3.0.1",
|
||||||
"popper.js": "^1.16.0",
|
"popper.js": "^1.16.0",
|
||||||
@ -113,8 +113,8 @@
|
|||||||
"del": "^5.1.0",
|
"del": "^5.1.0",
|
||||||
"devtron": "^1.4.0",
|
"devtron": "^1.4.0",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"electron": "^6.1.0",
|
"electron": "7.0.0",
|
||||||
"electron-builder": "^21.2.0",
|
"electron-builder": "^22.1.0",
|
||||||
"electron-devtools-installer": "^2.2.4",
|
"electron-devtools-installer": "^2.2.4",
|
||||||
"electron-rebuild": "^1.8.6",
|
"electron-rebuild": "^1.8.6",
|
||||||
"electron-updater": "^4.1.2",
|
"electron-updater": "^4.1.2",
|
||||||
@ -153,7 +153,7 @@
|
|||||||
"postcss-preset-env": "^6.6.0",
|
"postcss-preset-env": "^6.6.0",
|
||||||
"raw-loader": "^3.1.0",
|
"raw-loader": "^3.1.0",
|
||||||
"require-dir": "^1.2.0",
|
"require-dir": "^1.2.0",
|
||||||
"spectron": "^8.0.0",
|
"spectron": "^9.0.0",
|
||||||
"style-loader": "^1.0.0",
|
"style-loader": "^1.0.0",
|
||||||
"svg-sprite-loader": "^4.1.6",
|
"svg-sprite-loader": "^4.1.6",
|
||||||
"svgo": "^1.3.0",
|
"svgo": "^1.3.0",
|
||||||
@ -171,9 +171,6 @@
|
|||||||
"webpack-hot-middleware": "^2.25.0",
|
"webpack-hot-middleware": "^2.25.0",
|
||||||
"webpack-merge": "^4.2.1"
|
"webpack-merge": "^4.2.1"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
|
||||||
"vscode-windows-registry": "^1.0.2"
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@github.com:marktext/marktext.git"
|
"url": "git@github.com:marktext/marktext.git"
|
||||||
|
@ -3,7 +3,7 @@ import fse from 'fs-extra'
|
|||||||
import { exec } from 'child_process'
|
import { exec } from 'child_process'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import log from 'electron-log'
|
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 { isChildOfDirectory } from 'common/filesystem/paths'
|
||||||
import { isLinux, isOsx, isWindows } from '../config'
|
import { isLinux, isOsx, isWindows } from '../config'
|
||||||
import parseArgs from '../cli/parser'
|
import parseArgs from '../cli/parser'
|
||||||
@ -115,7 +115,7 @@ class App {
|
|||||||
const { paths } = this._accessor
|
const { paths } = this._accessor
|
||||||
ensureDefaultDict(paths.userDataPath)
|
ensureDefaultDict(paths.userDataPath)
|
||||||
.catch(error => {
|
.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) {
|
if (startUpAction === 'folder' && defaultDirectoryToOpen) {
|
||||||
const info = normalizeMarkdownPath(defaultDirectoryToOpen)
|
const info = normalizeMarkdownPath(defaultDirectoryToOpen)
|
||||||
if (info) {
|
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) {
|
if (isOsx) {
|
||||||
app.dock.setMenu(dockMenu)
|
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) {
|
} else if (isWindows) {
|
||||||
app.setJumpList([{
|
app.setJumpList([{
|
||||||
type: 'recent'
|
type: 'recent'
|
||||||
|
@ -16,7 +16,7 @@ export const editorWinOptions = {
|
|||||||
zoomFactor: 1.0
|
zoomFactor: 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultPreferenceWinOptions = {
|
export const preferencesWinOptions = {
|
||||||
width: 950,
|
width: 950,
|
||||||
height: 650,
|
height: 650,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
|
@ -71,7 +71,7 @@ class DataCenter extends EventEmitter {
|
|||||||
|
|
||||||
return Object.assign(data, encryptObj)
|
return Object.assign(data, encryptObj)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(err)
|
log.error('Failed to decrypt secure keys:', err)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@ class DataCenter extends EventEmitter {
|
|||||||
try {
|
try {
|
||||||
return await keytar.setPassword(serviceName, key, value)
|
return await keytar.setPassword(serviceName, key, value)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(err)
|
log.error('dataCenter::setItem:', err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return this.store.set(key, value)
|
return this.store.set(key, value)
|
||||||
|
@ -235,7 +235,7 @@ class Watcher {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} 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.
|
// Catch errors that may come from invalid configuration files like settings.
|
||||||
const msgHint = err.message.includes('Config schema violation')
|
const msgHint = err.message.includes('Config schema violation')
|
||||||
? 'This seems to be an issue with your configuration file(s). ' : ''
|
? 'This seems to be an issue with your configuration file(s). ' : ''
|
||||||
|
log.error(`Loading Mark Text failed during initialization! ${msgHint}`, err)
|
||||||
log.error(`Loading Mark Text failed during initialization! ${msgHint}`)
|
|
||||||
log.error(err)
|
|
||||||
|
|
||||||
const EXIT_ON_ERROR = !!process.env.MARKTEXT_EXIT_ON_ERROR
|
const EXIT_ON_ERROR = !!process.env.MARKTEXT_EXIT_ON_ERROR
|
||||||
const SHOW_ERROR_DIALOG = !process.env.MARKTEXT_ERROR_INTERACTION
|
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 })
|
win.webContents.send('AGANI::export-success', { type, filePath })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(err)
|
log.error('Error while exporting:', err)
|
||||||
const ERROR_MSG = err.message || `Error happened when export ${filePath}`
|
const ERROR_MSG = err.message || `Error happened when export ${filePath}`
|
||||||
win.webContents.send('AGANI::show-notification', {
|
win.webContents.send('AGANI::show-notification', {
|
||||||
title: 'Export failure',
|
title: 'Export failure',
|
||||||
@ -80,19 +80,9 @@ const handleResponseForExport = async (e, { type, content, pathname, title, page
|
|||||||
|
|
||||||
const handleResponseForPrint = e => {
|
const handleResponseForPrint = e => {
|
||||||
const win = BrowserWindow.fromWebContents(e.sender)
|
const win = BrowserWindow.fromWebContents(e.sender)
|
||||||
|
win.webContents.print({ printBackground: true }, () => {
|
||||||
// See GH#749, Electron#16085 and Electron#17523.
|
removePrintServiceFromWindow(win)
|
||||||
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)
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleResponseForSave = async (e, { id, filename, markdown, pathname, options, defaultPath }) => {
|
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
|
return id
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
log.error(err)
|
log.error('Error while saving:', err)
|
||||||
win.webContents.send('mt::tab-save-failure', id, err.message)
|
win.webContents.send('mt::tab-save-failure', id, err.message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -185,7 +175,7 @@ const openPandocFile = async (windowId, pathname) => {
|
|||||||
const data = await converter()
|
const data = await converter()
|
||||||
ipcMain.emit('app-open-markdown-by-id', windowId, data)
|
ipcMain.emit('app-open-markdown-by-id', windowId, data)
|
||||||
} catch (err) {
|
} 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)
|
win.send('mt::force-close-tabs-by-id', tabIds)
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
log.error(err.error)
|
log.error('Error while save all:', err.error)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const tabIds = unsavedFiles.map(f => f.id)
|
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 => {
|
.catch(err => {
|
||||||
log.error(err)
|
log.error('Error while save as:', err)
|
||||||
win.webContents.send('mt::tab-save-failure', id, err.message)
|
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)
|
ipcMain.emit('window-close-by-id', win.id)
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.log(err)
|
log.error('Error while saving before quit:', err)
|
||||||
log.error(err)
|
|
||||||
|
|
||||||
// Notify user about the problem.
|
// Notify user about the problem.
|
||||||
dialog.showMessageBox(win, {
|
dialog.showMessageBox(win, {
|
||||||
@ -446,19 +435,9 @@ export const importFile = async win => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const print = win => {
|
export const print = win => {
|
||||||
if (!win) {
|
if (win) {
|
||||||
return
|
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 => {
|
export const openFile = async win => {
|
||||||
|
@ -9,13 +9,13 @@ export const toggleAlwaysOnTop = win => {
|
|||||||
export const zoomIn = win => {
|
export const zoomIn = win => {
|
||||||
const { webContents } = win
|
const { webContents } = win
|
||||||
const zoom = webContents.getZoomFactor()
|
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))
|
webContents.send('mt::window-zoom', Math.min(2.0, zoom + 0.125))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const zoomOut = win => {
|
export const zoomOut = win => {
|
||||||
const { webContents } = win
|
const { webContents } = win
|
||||||
const zoom = webContents.getZoomFactor()
|
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))
|
webContents.send('mt::window-zoom', Math.max(1.0, zoom - 0.125))
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ class AppMenu {
|
|||||||
}
|
}
|
||||||
return recentDocuments
|
return recentDocuments
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(err)
|
log.error('Error while read recently used documents:', err)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -166,7 +166,7 @@ export default function (keybindings) {
|
|||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
id: 'frontMatterMenuItem',
|
id: 'frontMatterMenuItem',
|
||||||
label: 'YAML Front Matter',
|
label: 'Front Matter',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
accelerator: keybindings.getAccelerator('paragraphYAMLFrontMatter'),
|
accelerator: keybindings.getAccelerator('paragraphYAMLFrontMatter'),
|
||||||
click (menuItem, browserWindow) {
|
click (menuItem, browserWindow) {
|
||||||
|
@ -17,19 +17,13 @@ export default function (keybindings) {
|
|||||||
toggleAlwaysOnTop(browserWindow)
|
toggleAlwaysOnTop(browserWindow)
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
// TODO: Disable due GH#1225.
|
|
||||||
visible: false,
|
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
}, {
|
}, {
|
||||||
// TODO: Disable due GH#1225.
|
|
||||||
visible: false,
|
|
||||||
label: 'Zoom In',
|
label: 'Zoom In',
|
||||||
click (menuItem, browserWindow) {
|
click (menuItem, browserWindow) {
|
||||||
zoomIn(browserWindow)
|
zoomIn(browserWindow)
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
// TODO: Disable due GH#1225.
|
|
||||||
visible: false,
|
|
||||||
label: 'Zoom Out',
|
label: 'Zoom Out',
|
||||||
click (menuItem, browserWindow) {
|
click (menuItem, browserWindow) {
|
||||||
zoomOut(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 path from 'path'
|
||||||
import EventEmitter from 'events'
|
import EventEmitter from 'events'
|
||||||
import Store from 'electron-store'
|
import Store from 'electron-store'
|
||||||
import { BrowserWindow, ipcMain, systemPreferences } from 'electron'
|
import { BrowserWindow, ipcMain, nativeTheme } from 'electron'
|
||||||
import log from 'electron-log'
|
import log from 'electron-log'
|
||||||
import { isOsx, isWindows } from '../config'
|
import { isWindows } from '../config'
|
||||||
import { hasSameKeys } from '../utils'
|
import { hasSameKeys } from '../utils'
|
||||||
import { getStringRegKey, winHKEY } from '../platform/win32/registry.js'
|
|
||||||
import schema from './schema'
|
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'
|
const PREFERENCES_FILE_NAME = 'preferences'
|
||||||
|
|
||||||
class Preference extends EventEmitter {
|
class Preference extends EventEmitter {
|
||||||
@ -50,7 +38,9 @@ class Preference extends EventEmitter {
|
|||||||
let defaultSettings = null
|
let defaultSettings = null
|
||||||
try {
|
try {
|
||||||
defaultSettings = fse.readJsonSync(this.staticPath)
|
defaultSettings = fse.readJsonSync(this.staticPath)
|
||||||
if (isDarkSystemMode()) {
|
|
||||||
|
// Set best theme on first application start.
|
||||||
|
if (nativeTheme.shouldUseDarkColors) {
|
||||||
defaultSettings.theme = 'dark'
|
defaultSettings.theme = 'dark'
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -242,11 +242,25 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
|
"footnote": {
|
||||||
|
"description": "Markdown-Enable pandoc's markdown extension footnote.",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
|
||||||
"theme": {
|
"theme": {
|
||||||
"description": "Theme--Select the theme used in Mark Text",
|
"description": "Theme--Select the theme used in Mark Text",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"autoSwitchTheme": {
|
||||||
|
"description": "Theme--Automatically adjust application theme according system.",
|
||||||
|
"default": 2,
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
"spellcheckerEnabled": {
|
"spellcheckerEnabled": {
|
||||||
"description": "Spelling--Whether spell checking is enabled.",
|
"description": "Spelling--Whether spell checking is enabled.",
|
||||||
|
@ -46,8 +46,9 @@ const filesHandler = (files, directory, key) => {
|
|||||||
|
|
||||||
const rebuild = (directory) => {
|
const rebuild = (directory) => {
|
||||||
fs.readdir(directory, (err, files) => {
|
fs.readdir(directory, (err, files) => {
|
||||||
if (err) log.error(err)
|
if (err) {
|
||||||
else {
|
log.error('imagePathAutoComplement::rebuild:', err)
|
||||||
|
} else {
|
||||||
filesHandler(files, directory)
|
filesHandler(files, directory)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -3,7 +3,7 @@ import { BrowserWindow, ipcMain } from 'electron'
|
|||||||
import electronLocalshortcut from '@hfelix/electron-localshortcut'
|
import electronLocalshortcut from '@hfelix/electron-localshortcut'
|
||||||
import BaseWindow, { WindowLifecycle, WindowType } from './base'
|
import BaseWindow, { WindowLifecycle, WindowType } from './base'
|
||||||
import { centerWindowOptions } from './utils'
|
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 {
|
class SettingWindow extends BaseWindow {
|
||||||
/**
|
/**
|
||||||
@ -21,12 +21,18 @@ class SettingWindow extends BaseWindow {
|
|||||||
*/
|
*/
|
||||||
createWindow (options = {}) {
|
createWindow (options = {}) {
|
||||||
const { menu: appMenu, env, keybindings, preferences } = this._accessor
|
const { menu: appMenu, env, keybindings, preferences } = this._accessor
|
||||||
const winOptions = Object.assign({}, defaultPreferenceWinOptions, options)
|
const winOptions = Object.assign({}, preferencesWinOptions, options)
|
||||||
centerWindowOptions(winOptions)
|
centerWindowOptions(winOptions)
|
||||||
if (isLinux) {
|
if (isLinux) {
|
||||||
winOptions.icon = path.join(__static, 'logo-96px.png')
|
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
|
// Enable native or custom/frameless window and titlebar
|
||||||
const { titleBarStyle, theme } = preferences.getAll()
|
const { titleBarStyle, theme } = preferences.getAll()
|
||||||
if (!isOsx) {
|
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;
|
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 {
|
.ag-highlight {
|
||||||
animation-name: highlight;
|
animation-name: highlight;
|
||||||
animation-duration: .25s;
|
animation-duration: .25s;
|
||||||
@ -1194,3 +1243,28 @@ figure:not(.ag-active) pre.ag-paragraph.line-numbers {
|
|||||||
top: .05em;
|
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_SELECTED',
|
||||||
'AG_INLINE_IMAGE_IS_EDIT',
|
'AG_INLINE_IMAGE_IS_EDIT',
|
||||||
'AG_INDENT_CODE',
|
'AG_INDENT_CODE',
|
||||||
|
'AG_INLINE_FOOTNOTE_IDENTIFIER',
|
||||||
'AG_INLINE_RULE',
|
'AG_INLINE_RULE',
|
||||||
'AG_LANGUAGE',
|
'AG_LANGUAGE',
|
||||||
'AG_LANGUAGE_INPUT',
|
'AG_LANGUAGE_INPUT',
|
||||||
@ -276,7 +277,8 @@ export const MUYA_DEFAULT_OPTION = {
|
|||||||
imagePathAutoComplete: () => [],
|
imagePathAutoComplete: () => [],
|
||||||
|
|
||||||
// Markdown extensions
|
// Markdown extensions
|
||||||
superSubScript: false
|
superSubScript: false,
|
||||||
|
footnote: false
|
||||||
}
|
}
|
||||||
|
|
||||||
// export const DIAGRAM_TEMPLATE = {
|
// export const DIAGRAM_TEMPLATE = {
|
||||||
|
@ -345,6 +345,29 @@ const backspaceCtrl = ContentState => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
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.type === 'span' &&
|
||||||
block.functionType === 'codeContent' &&
|
block.functionType === 'codeContent' &&
|
||||||
left === 0 &&
|
left === 0 &&
|
||||||
@ -492,7 +515,7 @@ const backspaceCtrl = ContentState => {
|
|||||||
// also need to remove the paragrah
|
// also need to remove the paragrah
|
||||||
if (this.isOnlyChild(block) && block.type === 'span') {
|
if (this.isOnlyChild(block) && block.type === 'span') {
|
||||||
this.removeBlock(parent)
|
this.removeBlock(parent)
|
||||||
} else if (block.functionType !== 'languageInput') {
|
} else if (block.functionType !== 'languageInput' && block.functionType !== 'footnoteInput') {
|
||||||
this.removeBlock(block)
|
this.removeBlock(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -500,10 +523,14 @@ const backspaceCtrl = ContentState => {
|
|||||||
start: { key, offset },
|
start: { key, offset },
|
||||||
end: { key, offset }
|
end: { key, offset }
|
||||||
}
|
}
|
||||||
if (this.isCollapse()) {
|
let needRenderAll = false
|
||||||
|
|
||||||
|
if (this.isCollapse() && preBlock.type === 'span' && preBlock.functionType === 'paragraphContent') {
|
||||||
this.checkInlineUpdate(preBlock)
|
this.checkInlineUpdate(preBlock)
|
||||||
|
needRenderAll = true
|
||||||
}
|
}
|
||||||
this.partialRender()
|
|
||||||
|
needRenderAll ? this.render() : this.partialRender()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -191,7 +191,7 @@ const copyCutCtrl = ContentState => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let htmlData = wrapper.innerHTML
|
let htmlData = wrapper.innerHTML
|
||||||
const textData = this.htmlToMarkdown(htmlData)
|
const textData = escapeHtml(this.htmlToMarkdown(htmlData))
|
||||||
htmlData = marked(textData)
|
htmlData = marked(textData)
|
||||||
|
|
||||||
return { html: htmlData, text: textData }
|
return { html: htmlData, text: textData }
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import selection from '../selection'
|
import selection from '../selection'
|
||||||
import { isOsx } from '../config'
|
import { isOsx } from '../config'
|
||||||
|
|
||||||
|
/* eslint-disable no-useless-escape */
|
||||||
|
const FOOTNOTE_REG = /^\[\^([^\^\[\]\s]+?)(?<!\\)\]:$/
|
||||||
|
/* eslint-enable no-useless-escape */
|
||||||
|
|
||||||
const checkAutoIndent = (text, offset) => {
|
const checkAutoIndent = (text, offset) => {
|
||||||
const pairStr = text.substring(offset - 1, offset + 1)
|
const pairStr = text.substring(offset - 1, offset + 1)
|
||||||
return /^(\{\}|\[\]|\(\)|><)$/.test(pairStr)
|
return /^(\{\}|\[\]|\(\)|><)$/.test(pairStr)
|
||||||
@ -226,6 +230,26 @@ const enterCtrl = ContentState => {
|
|||||||
return this.enterHandler(event)
|
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`
|
// 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`
|
// only cursor in `line block` can create `soft line break` and `hard line break`
|
||||||
// handle line in code block
|
// handle line in code block
|
||||||
@ -418,6 +442,7 @@ const enterCtrl = ContentState => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.insertAfter(newBlock, block)
|
this.insertAfter(newBlock, block)
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case left === 0 && right === 0: {
|
case left === 0 && right === 0: {
|
||||||
@ -511,7 +536,14 @@ const enterCtrl = ContentState => {
|
|||||||
end: { key, offset }
|
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 }) => {
|
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) {
|
switch (type) {
|
||||||
case 'em':
|
case 'em':
|
||||||
case 'del':
|
case 'del':
|
||||||
|
@ -29,7 +29,7 @@ const imageCtrl = ContentState => {
|
|||||||
// Only encode URLs but not local paths or data URLs
|
// Only encode URLs but not local paths or data URLs
|
||||||
let imgUrl
|
let imgUrl
|
||||||
if (!/data:image/.test(src)) {
|
if (!/data:image/.test(src)) {
|
||||||
imgUrl = encodeURI(src)
|
imgUrl = encodeURI(src).replace(/#/g, encodeURIComponent('#'))
|
||||||
} else {
|
} else {
|
||||||
imgUrl = src
|
imgUrl = src
|
||||||
}
|
}
|
||||||
@ -132,7 +132,7 @@ const imageCtrl = ContentState => {
|
|||||||
}
|
}
|
||||||
imageText += ']('
|
imageText += ']('
|
||||||
if (src) {
|
if (src) {
|
||||||
imageText += encodeURI(src)
|
imageText += encodeURI(src).replace(/#/g, encodeURIComponent('#'))
|
||||||
}
|
}
|
||||||
if (title) {
|
if (title) {
|
||||||
imageText += ` "${title}"`
|
imageText += ` "${title}"`
|
||||||
@ -177,11 +177,19 @@ const imageCtrl = ContentState => {
|
|||||||
this.selectedImage = imageInfo
|
this.selectedImage = imageInfo
|
||||||
const { key } = imageInfo
|
const { key } = imageInfo
|
||||||
const block = this.getBlock(key)
|
const block = this.getBlock(key)
|
||||||
|
const outMostBlock = this.findOutMostBlock(block)
|
||||||
this.cursor = {
|
this.cursor = {
|
||||||
start: { key, offset: imageInfo.token.range.end },
|
start: { key, offset: imageInfo.token.range.end },
|
||||||
end: { 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 imageCtrl from './imageCtrl'
|
||||||
import linkCtrl from './linkCtrl'
|
import linkCtrl from './linkCtrl'
|
||||||
import dragDropCtrl from './dragDropCtrl'
|
import dragDropCtrl from './dragDropCtrl'
|
||||||
|
import footnoteCtrl from './footnoteCtrl'
|
||||||
import importMarkdown from '../utils/importMarkdown'
|
import importMarkdown from '../utils/importMarkdown'
|
||||||
import Cursor from '../selection/cursor'
|
import Cursor from '../selection/cursor'
|
||||||
import escapeCharactersMap, { escapeCharacters } from '../parser/escapeCharacter'
|
import escapeCharactersMap, { escapeCharacters } from '../parser/escapeCharacter'
|
||||||
@ -58,6 +59,7 @@ const prototypes = [
|
|||||||
imageCtrl,
|
imageCtrl,
|
||||||
linkCtrl,
|
linkCtrl,
|
||||||
dragDropCtrl,
|
dragDropCtrl,
|
||||||
|
footnoteCtrl,
|
||||||
importMarkdown
|
importMarkdown
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ const INLINE_UPDATE_FRAGMENTS = [
|
|||||||
'^(?:[\\s\\S]+?)\\n {0,3}(\\={3,}|\\-{3,})(?= {1,}|$)', // Setext headings **match from beginning**
|
'^(?:[\\s\\S]+?)\\n {0,3}(\\={3,}|\\-{3,})(?= {1,}|$)', // Setext headings **match from beginning**
|
||||||
'(?:^|\n) {0,3}(>).+', // Block quote
|
'(?:^|\n) {0,3}(>).+', // Block quote
|
||||||
'^( {4,})', // Indent code **match from beginning**
|
'^( {4,})', // Indent code **match from beginning**
|
||||||
|
'^(\\[\\^[^\\^\\[\\]\\s]+?(?<!\\\\)\\]: )', // Footnote **match from beginning**
|
||||||
'(?:^|\n) {0,3}((?:\\* *\\* *\\*|- *- *-|_ *_ *_)[ \\*\\-\\_]*)$' // Thematic break
|
'(?:^|\n) {0,3}((?:\\* *\\* *\\*|- *- *-|_ *_ *_)[ \\*\\-\\_]*)$' // Thematic break
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -76,7 +77,7 @@ const updateCtrl = ContentState => {
|
|||||||
if (/figure/.test(block.type)) {
|
if (/figure/.test(block.type)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (/cellContent|codeContent|languageInput/.test(block.functionType)) {
|
if (/cellContent|codeContent|languageInput|footnoteInput/.test(block.functionType)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,8 +90,9 @@ const updateCtrl = ContentState => {
|
|||||||
const listItem = this.getParent(block)
|
const listItem = this.getParent(block)
|
||||||
const [
|
const [
|
||||||
match, bullet, tasklist, order, atxHeader,
|
match, bullet, tasklist, order, atxHeader,
|
||||||
setextHeader, blockquote, indentCode, hr
|
setextHeader, blockquote, indentCode, footnote, hr
|
||||||
] = text.match(INLINE_UPDATE_REG) || []
|
] = text.match(INLINE_UPDATE_REG) || []
|
||||||
|
const { footnote: isSupportFootnote } = this.muya.options
|
||||||
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case (!!hr && new Set(hr.split('').filter(i => /\S/.test(i))).size === 1):
|
case (!!hr && new Set(hr.split('').filter(i => /\S/.test(i))).size === 1):
|
||||||
@ -118,6 +120,9 @@ const updateCtrl = ContentState => {
|
|||||||
case !!indentCode:
|
case !!indentCode:
|
||||||
return this.updateIndentCode(block, line)
|
return this.updateIndentCode(block, line)
|
||||||
|
|
||||||
|
case !!footnote && block.type === 'p' && !block.parent && isSupportFootnote:
|
||||||
|
return this.updateFootnote(block, line)
|
||||||
|
|
||||||
case !match:
|
case !match:
|
||||||
default:
|
default:
|
||||||
return this.updateToParagraph(block, line)
|
return this.updateToParagraph(block, line)
|
||||||
|
@ -101,6 +101,7 @@ class ClickEvent {
|
|||||||
const rubyRender = target.closest(`.${CLASS_OR_ID.AG_RUBY_RENDER}`)
|
const rubyRender = target.closest(`.${CLASS_OR_ID.AG_RUBY_RENDER}`)
|
||||||
const imageWrapper = target.closest(`.${CLASS_OR_ID.AG_INLINE_IMAGE}`)
|
const imageWrapper = target.closest(`.${CLASS_OR_ID.AG_INLINE_IMAGE}`)
|
||||||
const codeCopy = target.closest('.ag-code-copy')
|
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 imageDelete = target.closest('.ag-image-icon-delete') || target.closest('.ag-image-icon-close')
|
||||||
const mathText = mathRender && mathRender.previousElementSibling
|
const mathText = mathRender && mathRender.previousElementSibling
|
||||||
const rubyText = rubyRender && rubyRender.previousElementSibling
|
const rubyText = rubyRender && rubyRender.previousElementSibling
|
||||||
@ -131,6 +132,20 @@ class ClickEvent {
|
|||||||
return contentState.deleteImage(imageInfo)
|
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
|
// Handle image click, to select the current image
|
||||||
if (target.tagName === 'IMG' && imageWrapper) {
|
if (target.tagName === 'IMG' && imageWrapper) {
|
||||||
// Handle select image
|
// Handle select image
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { getLinkInfo } from '../utils/getLinkInfo'
|
import { getLinkInfo } from '../utils/getLinkInfo'
|
||||||
|
import { collectFootnotes } from '../utils'
|
||||||
|
|
||||||
class MouseEvent {
|
class MouseEvent {
|
||||||
constructor (muya) {
|
constructor (muya) {
|
||||||
@ -12,8 +13,9 @@ class MouseEvent {
|
|||||||
const handler = event => {
|
const handler = event => {
|
||||||
const target = event.target
|
const target = event.target
|
||||||
const parent = target.parentNode
|
const parent = target.parentNode
|
||||||
const { hideLinkPopup } = this.muya.options
|
const preSibling = target.previousElementSibling
|
||||||
if (!hideLinkPopup && parent && parent.tagName === 'A' && parent.classList.contains('ag-inline-rule')) {
|
const parentPreSibling = parent ? parent.previousElementSibling : null
|
||||||
|
const { hideLinkPopup, footnote } = this.muya.options
|
||||||
const rect = parent.getBoundingClientRect()
|
const rect = parent.getBoundingClientRect()
|
||||||
const reference = {
|
const reference = {
|
||||||
getBoundingClientRect () {
|
getBoundingClientRect () {
|
||||||
@ -21,21 +23,59 @@ class MouseEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!hideLinkPopup &&
|
||||||
|
parent &&
|
||||||
|
parent.tagName === 'A' &&
|
||||||
|
parent.classList.contains('ag-inline-rule') &&
|
||||||
|
parentPreSibling &&
|
||||||
|
parentPreSibling.classList.contains('ag-hide')
|
||||||
|
) {
|
||||||
eventCenter.dispatch('muya-link-tools', {
|
eventCenter.dispatch('muya-link-tools', {
|
||||||
reference,
|
reference,
|
||||||
linkInfo: getLinkInfo(parent)
|
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 leaveHandler = event => {
|
||||||
const target = event.target
|
const target = event.target
|
||||||
const parent = target.parentNode
|
const parent = target.parentNode
|
||||||
|
const preSibling = target.previousElementSibling
|
||||||
|
const { footnote } = this.muya.options
|
||||||
if (parent && parent.tagName === 'A' && parent.classList.contains('ag-inline-rule')) {
|
if (parent && parent.tagName === 'A' && parent.classList.contains('ag-inline-rule')) {
|
||||||
eventCenter.dispatch('muya-link-tools', {
|
eventCenter.dispatch('muya-link-tools', {
|
||||||
reference: null
|
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)
|
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 originSrc = src
|
||||||
const tokens = []
|
const tokens = []
|
||||||
let pending = ''
|
let pending = ''
|
||||||
let pendingStartPos = pos
|
let pendingStartPos = pos
|
||||||
|
const { superSubScript, footnote } = options
|
||||||
const pushPending = () => {
|
const pushPending = () => {
|
||||||
if (pending) {
|
if (pending) {
|
||||||
tokens.push({
|
tokens.push({
|
||||||
@ -151,7 +151,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
|
|||||||
range,
|
range,
|
||||||
marker,
|
marker,
|
||||||
parent: tokens,
|
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]
|
backlash: to[3]
|
||||||
})
|
})
|
||||||
src = src.substring(to[0].length)
|
src = src.substring(to[0].length)
|
||||||
@ -192,7 +192,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
|
|||||||
range,
|
range,
|
||||||
marker,
|
marker,
|
||||||
parent: tokens,
|
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]
|
backlash: to[3]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -203,7 +203,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
|
|||||||
}
|
}
|
||||||
if (inChunk) continue
|
if (inChunk) continue
|
||||||
// superscript and subscript
|
// superscript and subscript
|
||||||
if (inlineRules.superscript && inlineRules.subscript) {
|
if (superSubScript) {
|
||||||
const superSubTo = inlineRules.superscript.exec(src) || inlineRules.subscript.exec(src)
|
const superSubTo = inlineRules.superscript.exec(src) || inlineRules.subscript.exec(src)
|
||||||
if (superSubTo) {
|
if (superSubTo) {
|
||||||
pushPending()
|
pushPending()
|
||||||
@ -223,6 +223,28 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
|
|||||||
continue
|
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
|
// image
|
||||||
const imageTo = inlineRules.image.exec(src)
|
const imageTo = inlineRules.image.exec(src)
|
||||||
correctUrl(imageTo)
|
correctUrl(imageTo)
|
||||||
@ -276,7 +298,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
|
|||||||
start: pos,
|
start: pos,
|
||||||
end: pos + linkTo[0].length
|
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: {
|
backlash: {
|
||||||
first: linkTo[3],
|
first: linkTo[3],
|
||||||
second: linkTo[5]
|
second: linkTo[5]
|
||||||
@ -306,7 +328,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
|
|||||||
start: pos,
|
start: pos,
|
||||||
end: pos + rLinkTo[0].length
|
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)
|
src = src.substring(rLinkTo[0].length)
|
||||||
@ -442,7 +464,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
|
|||||||
parent: tokens,
|
parent: tokens,
|
||||||
attrs,
|
attrs,
|
||||||
content: htmlTo[4],
|
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: {
|
range: {
|
||||||
start: pos,
|
start: pos,
|
||||||
end: pos + len
|
end: pos + len
|
||||||
@ -530,16 +552,8 @@ export const tokenizer = (src, {
|
|||||||
labels = new Map(),
|
labels = new Map(),
|
||||||
options = {}
|
options = {}
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const { superSubScript } = options
|
const rules = Object.assign({}, inlineRules, inlineExtensionRules)
|
||||||
|
const tokens = tokenizerFac(src, hasBeginRules ? beginRules : null, rules, 0, true, labels, 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 postTokenizer = tokens => {
|
const postTokenizer = tokens => {
|
||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
|
@ -35,7 +35,8 @@ export const block = {
|
|||||||
|
|
||||||
// extra
|
// extra
|
||||||
frontmatter: /^(?:(?:---\n([\s\S]+?)---)|(?:\+\+\+\n([\s\S]+?)\+\+\+)|(?:;;;\n([\s\S]+?);;;)|(?:\{\n([\s\S]+?)\}))(?:\n{2,}|\n{1,2}$)/,
|
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*\])(?:\\[\[\]]|[^\[\]])+/
|
block._label = /(?!\s*\])(?:\\[\[\]]|[^\[\]])+/
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import Renderer from './renderer'
|
import Renderer from './renderer'
|
||||||
import { normal, breaks, gfm, pedantic } from './inlineRules'
|
import { normal, breaks, gfm, pedantic } from './inlineRules'
|
||||||
import defaultOptions from './options'
|
import defaultOptions from './options'
|
||||||
import { escape, findClosingBracket } from './utils'
|
import { escape, findClosingBracket, getUniqueId } from './utils'
|
||||||
import { validateEmphasize, lowerPriority } from '../utils'
|
import { validateEmphasize, lowerPriority } from '../utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inline Lexer & Compiler
|
* Inline Lexer & Compiler
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function InlineLexer (links, options) {
|
function InlineLexer (links, footnotes, options) {
|
||||||
this.options = options || defaultOptions
|
this.options = options || defaultOptions
|
||||||
this.links = links
|
this.links = links
|
||||||
|
this.footnotes = footnotes
|
||||||
this.rules = normal
|
this.rules = normal
|
||||||
this.renderer = this.options.renderer || new Renderer()
|
this.renderer = this.options.renderer || new Renderer()
|
||||||
this.renderer.options = this.options
|
this.renderer.options = this.options
|
||||||
@ -49,7 +50,7 @@ function InlineLexer (links, options) {
|
|||||||
InlineLexer.prototype.output = function (src) {
|
InlineLexer.prototype.output = function (src) {
|
||||||
// src = src
|
// src = src
|
||||||
// .replace(/\u00a0/g, ' ')
|
// .replace(/\u00a0/g, ' ')
|
||||||
const { disableInline, emoji, math, superSubScript } = this.options
|
const { disableInline, emoji, math, superSubScript, footnote } = this.options
|
||||||
if (disableInline) {
|
if (disableInline) {
|
||||||
return escape(src)
|
return escape(src)
|
||||||
}
|
}
|
||||||
@ -73,6 +74,19 @@ InlineLexer.prototype.output = function (src) {
|
|||||||
continue
|
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
|
// tag
|
||||||
cap = this.rules.tag.exec(src)
|
cap = this.rules.tag.exec(src)
|
||||||
if (cap) {
|
if (cap) {
|
||||||
|
@ -29,7 +29,7 @@ const inline = {
|
|||||||
// ------------------------
|
// ------------------------
|
||||||
// patched
|
// 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
|
text: /^(`+|[^`])(?:[\s\S]*?(?:(?=[\\<!\[`*\$^]|\b_|$)|[^ ](?= {2,}\n))|(?= {2,}\n))/, // emoji is patched in gfm
|
||||||
|
|
||||||
// ------------------------
|
// ------------------------
|
||||||
@ -41,7 +41,8 @@ const inline = {
|
|||||||
|
|
||||||
// superscript and subScript
|
// superscript and subScript
|
||||||
superscript: /^(\^)((?:[^\^\s]|(?<=\\)\1|(?<=\\) )+?)(?<!\\)\1(?!\1)/,
|
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
|
// list of punctuation marks from common mark spec
|
||||||
@ -114,7 +115,7 @@ export const gfm = Object.assign({}, normal, {
|
|||||||
// ------------------------
|
// ------------------------
|
||||||
// patched
|
// 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.!#$%&'*+\/=?_`{\|}~-]+@))/,
|
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 { normal, gfm, pedantic } from './blockRules'
|
||||||
import options from './options'
|
import options from './options'
|
||||||
import { splitCells, rtrim } from './utils'
|
import { splitCells, rtrim, getUniqueId } from './utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Block Lexer
|
* Block Lexer
|
||||||
@ -9,6 +9,8 @@ import { splitCells, rtrim } from './utils'
|
|||||||
function Lexer (opts) {
|
function Lexer (opts) {
|
||||||
this.tokens = []
|
this.tokens = []
|
||||||
this.tokens.links = Object.create(null)
|
this.tokens.links = Object.create(null)
|
||||||
|
this.tokens.footnotes = Object.create(null)
|
||||||
|
this.footnoteOrder = 0
|
||||||
this.options = Object.assign({}, options, opts)
|
this.options = Object.assign({}, options, opts)
|
||||||
this.rules = normal
|
this.rules = normal
|
||||||
|
|
||||||
@ -28,7 +30,32 @@ Lexer.prototype.lex = function (src) {
|
|||||||
.replace(/\r\n|\r/g, '\n')
|
.replace(/\r\n|\r/g, '\n')
|
||||||
.replace(/\t/g, ' ')
|
.replace(/\t/g, ' ')
|
||||||
this.checkFrontmatter = true
|
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) {
|
Lexer.prototype.token = function (src, top) {
|
||||||
const { frontMatter, math } = this.options
|
const { frontMatter, math, footnote } = this.options
|
||||||
src = src.replace(/^ +$/gm, '')
|
src = src.replace(/^ +$/gm, '')
|
||||||
|
|
||||||
let loose
|
let loose
|
||||||
@ -48,7 +75,6 @@ Lexer.prototype.token = function (src, top) {
|
|||||||
let i
|
let i
|
||||||
let tag
|
let tag
|
||||||
let l
|
let l
|
||||||
let checked
|
|
||||||
|
|
||||||
// Only check front matter at the begining of a markdown file.
|
// Only check front matter at the begining of a markdown file.
|
||||||
// Please see note in "blockquote" why we need "checkFrontmatter" and "top".
|
// 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
|
// fences
|
||||||
cap = this.rules.fences.exec(src)
|
cap = this.rules.fences.exec(src)
|
||||||
if (cap) {
|
if (cap) {
|
||||||
@ -233,6 +290,7 @@ Lexer.prototype.token = function (src, top) {
|
|||||||
// list
|
// list
|
||||||
cap = this.rules.list.exec(src)
|
cap = this.rules.list.exec(src)
|
||||||
if (cap) {
|
if (cap) {
|
||||||
|
let checked
|
||||||
src = src.substring(cap[0].length)
|
src = src.substring(cap[0].length)
|
||||||
bull = cap[2]
|
bull = cap[2]
|
||||||
let isOrdered = bull.length > 1
|
let isOrdered = bull.length > 1
|
||||||
@ -367,7 +425,7 @@ Lexer.prototype.token = function (src, top) {
|
|||||||
|
|
||||||
const isOrderedListItem = /\d/.test(bull)
|
const isOrderedListItem = /\d/.test(bull)
|
||||||
this.tokens.push({
|
this.tokens.push({
|
||||||
checked: checked,
|
checked,
|
||||||
listItemType: bull.length > 1 ? 'order' : (isTaskList ? 'task' : 'bullet'),
|
listItemType: bull.length > 1 ? 'order' : (isTaskList ? 'task' : 'bullet'),
|
||||||
bulletMarkerOrDelimiter: isOrderedListItem ? bull.slice(-1) : bull.charAt(0),
|
bulletMarkerOrDelimiter: isOrderedListItem ? bull.slice(-1) : bull.charAt(0),
|
||||||
type: loose ? 'loose_item_start' : 'list_item_start'
|
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))
|
throw new Error('Infinite loop on byte: ' + src.charCodeAt(0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.tokens
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Lexer
|
export default Lexer
|
||||||
|
@ -28,5 +28,6 @@ export default {
|
|||||||
emoji: true,
|
emoji: true,
|
||||||
math: true,
|
math: true,
|
||||||
frontMatter: true,
|
frontMatter: true,
|
||||||
superSubScript: false
|
superSubScript: false,
|
||||||
|
footnote: false
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ import defaultOptions from './options'
|
|||||||
function Parser (options) {
|
function Parser (options) {
|
||||||
this.tokens = []
|
this.tokens = []
|
||||||
this.token = null
|
this.token = null
|
||||||
|
this.footnotes = null
|
||||||
|
this.footnoteIdentifier = ''
|
||||||
this.options = options || defaultOptions
|
this.options = options || defaultOptions
|
||||||
this.options.renderer = this.options.renderer || new Renderer()
|
this.options.renderer = this.options.renderer || new Renderer()
|
||||||
this.renderer = this.options.renderer
|
this.renderer = this.options.renderer
|
||||||
@ -23,14 +25,15 @@ function Parser (options) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
Parser.prototype.parse = function (src) {
|
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
|
// use an InlineLexer with a TextRenderer to extract pure text
|
||||||
this.inlineText = new InlineLexer(
|
this.inlineText = new InlineLexer(
|
||||||
src.links,
|
src.links,
|
||||||
|
src.footnotes,
|
||||||
Object.assign({}, this.options, { renderer: new TextRenderer() })
|
Object.assign({}, this.options, { renderer: new TextRenderer() })
|
||||||
)
|
)
|
||||||
this.tokens = src.reverse()
|
this.tokens = src.reverse()
|
||||||
|
this.footnotes = src.footnotes
|
||||||
let out = ''
|
let out = ''
|
||||||
while (this.next()) {
|
while (this.next()) {
|
||||||
out += this.tok()
|
out += this.tok()
|
||||||
@ -148,6 +151,27 @@ Parser.prototype.tok = function () {
|
|||||||
|
|
||||||
return this.renderer.blockquote(body)
|
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': {
|
case 'list_start': {
|
||||||
let body = ''
|
let body = ''
|
||||||
let taskList = false
|
let taskList = false
|
||||||
|
@ -44,6 +44,18 @@ Renderer.prototype.script = function (content, marker) {
|
|||||||
return `<${tagName}>${content}</${tagName}>`
|
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) {
|
Renderer.prototype.code = function (code, infostring, escaped, codeBlockStyle) {
|
||||||
const lang = (infostring || '').match(/\S*/)[0]
|
const lang = (infostring || '').match(/\S*/)[0]
|
||||||
if (this.options.highlight) {
|
if (this.options.highlight) {
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
* Helpers
|
* Helpers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
let uniqueIdCounter = 0
|
||||||
|
|
||||||
|
export const getUniqueId = () => ++uniqueIdCounter
|
||||||
|
|
||||||
export const escape = function escape (html, encode) {
|
export const escape = function escape (html, encode) {
|
||||||
if (encode) {
|
if (encode) {
|
||||||
if (escape.escapeTest.test(html)) {
|
if (escape.escapeTest.test(html)) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { CLASS_OR_ID } from '../../../config'
|
import { CLASS_OR_ID } from '../../../config'
|
||||||
import { renderTableTools } from './renderToolBar'
|
import { renderTableTools } from './renderToolBar'
|
||||||
|
import { footnoteJumpIcon } from './renderFootnoteJump'
|
||||||
import { renderEditIcon } from './renderContainerEditIcon'
|
import { renderEditIcon } from './renderContainerEditIcon'
|
||||||
import renderLineNumberRows from './renderLineNumber'
|
import renderLineNumberRows from './renderLineNumber'
|
||||||
import renderCopyButton from './renderCopyButton'
|
import renderCopyButton from './renderCopyButton'
|
||||||
@ -138,10 +139,12 @@ export default function renderContainerBlock (parent, block, activeBlocks, match
|
|||||||
} else if (type === 'figure') {
|
} else if (type === 'figure') {
|
||||||
if (functionType) {
|
if (functionType) {
|
||||||
Object.assign(data.dataset, { role: functionType.toUpperCase() })
|
Object.assign(data.dataset, { role: functionType.toUpperCase() })
|
||||||
if (functionType === 'table') {
|
if (functionType === 'table' && activeBlocks[0] && activeBlocks[0].functionType === 'cellContent') {
|
||||||
children.unshift(renderTableTools(activeBlocks))
|
children.unshift(renderTableTools(activeBlocks))
|
||||||
} else {
|
} else if (functionType !== 'footnote') {
|
||||||
children.unshift(renderEditIcon())
|
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 sequenceIcon from '../../../assets/pngicon/sequence/2.png'
|
||||||
import mermaidIcon from '../../../assets/pngicon/mermaid/2.png'
|
import mermaidIcon from '../../../assets/pngicon/mermaid/2.png'
|
||||||
import vegaIcon from '../../../assets/pngicon/chart/2.png'
|
import vegaIcon from '../../../assets/pngicon/chart/2.png'
|
||||||
|
import footnoteIcon from '../../../assets/pngicon/footnote/2.png'
|
||||||
|
|
||||||
const FUNCTION_TYPE_HASH = {
|
const FUNCTION_TYPE_HASH = {
|
||||||
mermaid: mermaidIcon,
|
mermaid: mermaidIcon,
|
||||||
@ -32,7 +33,8 @@ const FUNCTION_TYPE_HASH = {
|
|||||||
multiplemath: mathblockIcon,
|
multiplemath: mathblockIcon,
|
||||||
fencecode: codeIcon,
|
fencecode: codeIcon,
|
||||||
indentcode: codeIcon,
|
indentcode: codeIcon,
|
||||||
frontmatter: frontMatterIcon
|
frontmatter: frontMatterIcon,
|
||||||
|
footnote: footnoteIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function renderIcon (block) {
|
export default function renderIcon (block) {
|
||||||
|
@ -101,7 +101,8 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
|
|||||||
functionType !== 'codeContent' &&
|
functionType !== 'codeContent' &&
|
||||||
functionType !== 'languageInput'
|
functionType !== 'languageInput'
|
||||||
) {
|
) {
|
||||||
const hasBeginRules = type === 'span'
|
const hasBeginRules = /paragraphContent|atxLine/.test(functionType)
|
||||||
|
|
||||||
tokens = tokenizer(text, {
|
tokens = tokenizer(text, {
|
||||||
highlights,
|
highlights,
|
||||||
hasBeginRules,
|
hasBeginRules,
|
||||||
@ -247,6 +248,8 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
|
|||||||
} else if (type === 'span' && functionType === 'languageInput') {
|
} else if (type === 'span' && functionType === 'languageInput') {
|
||||||
const html = getHighlightHtml(text, highlights)
|
const html = getHighlightHtml(text, highlights)
|
||||||
children = htmlToVNode(html)
|
children = htmlToVNode(html)
|
||||||
|
} else if (type === 'span' && functionType === 'footnoteInput') {
|
||||||
|
Object.assign(data.attrs, { spellcheck: 'false' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!block.parent) {
|
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 referenceLink from './referenceLink'
|
||||||
import referenceImage from './referenceImage'
|
import referenceImage from './referenceImage'
|
||||||
import superSubScript from './superSubScript'
|
import superSubScript from './superSubScript'
|
||||||
|
import footnoteIdentifier from './footnoteIdentifier'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
backlashInToken,
|
backlashInToken,
|
||||||
@ -59,5 +60,6 @@ export default {
|
|||||||
htmlRuby,
|
htmlRuby,
|
||||||
referenceLink,
|
referenceLink,
|
||||||
referenceImage,
|
referenceImage,
|
||||||
superSubScript
|
superSubScript,
|
||||||
|
footnoteIdentifier
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ export const inlineRules = {
|
|||||||
export const inlineExtensionRules = {
|
export const inlineExtensionRules = {
|
||||||
// This is not the best regexp, because it not support `2^2\\^`.
|
// This is not the best regexp, because it not support `2^2\\^`.
|
||||||
superscript: /^(\^)((?:[^\^\s]|(?<=\\)\1|(?<=\\) )+?)(?<!\\)\1(?!\1)/,
|
superscript: /^(\^)((?:[^\^\s]|(?<=\\)\1|(?<=\\) )+?)(?<!\\)\1(?!\1)/,
|
||||||
subscript: /^(~)((?:[^~\s]|(?<=\\)\1|(?<=\\) )+?)(?<!\\)\1(?!\1)/
|
subscript: /^(~)((?:[^~\s]|(?<=\\)\1|(?<=\\) )+?)(?<!\\)\1(?!\1)/,
|
||||||
|
footnote_identifier: /^(\[\^)([^\^\[\]\s]+?)(?<!\\)\]/
|
||||||
}
|
}
|
||||||
/* eslint-enable no-useless-escape */
|
/* 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 linkIcon from '../../assets/pngicon/format_link/2.png'
|
||||||
import strikeIcon from '../../assets/pngicon/format_strike/2.png'
|
import strikeIcon from '../../assets/pngicon/format_strike/2.png'
|
||||||
import mathIcon from '../../assets/pngicon/format_math/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'
|
import clearIcon from '../../assets/pngicon/format_clear/2.png'
|
||||||
|
|
||||||
const COMMAND_KEY = isOsx ? '⌘' : '⌃'
|
const COMMAND_KEY = isOsx ? '⌘' : '⌃'
|
||||||
@ -32,6 +33,11 @@ const icons = [
|
|||||||
tooltip: 'Strikethrough',
|
tooltip: 'Strikethrough',
|
||||||
shortcut: `${COMMAND_KEY}+D`,
|
shortcut: `${COMMAND_KEY}+D`,
|
||||||
icon: strikeIcon
|
icon: strikeIcon
|
||||||
|
}, {
|
||||||
|
type: 'mark',
|
||||||
|
tooltip: 'Highlight',
|
||||||
|
shortcut: `⇧+${COMMAND_KEY}+H`,
|
||||||
|
icon: highlightIcon
|
||||||
}, {
|
}, {
|
||||||
type: 'inline_code',
|
type: 'inline_code',
|
||||||
tooltip: 'Inline Code',
|
tooltip: 'Inline Code',
|
||||||
|
@ -49,8 +49,8 @@
|
|||||||
|
|
||||||
.ag-format-picker li.item .icon-wrapper {
|
.ag-format-picker li.item .icon-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 14px;
|
width: 16px;
|
||||||
height: 14px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-format-picker li.item .icon-wrapper i.icon {
|
.ag-format-picker li.item .icon-wrapper i.icon {
|
||||||
@ -67,9 +67,9 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
filter: drop-shadow(14px 0 currentColor);
|
filter: drop-shadow(16px 0 currentColor);
|
||||||
position: relative;
|
position: relative;
|
||||||
left: -14px;
|
left: -16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-format-picker li.item.active .icon-wrapper i.icon {
|
.ag-format-picker li.item.active .icon-wrapper i.icon {
|
||||||
|
@ -33,16 +33,16 @@
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 14px;
|
width: 16px;
|
||||||
height: 14px;
|
height: 16px;
|
||||||
color: var(--iconColor);
|
color: var(--iconColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-front-menu li.item .icon-wrapper i.icon {
|
.ag-front-menu li.item .icon-wrapper i.icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 14px;
|
height: 16px;
|
||||||
width: 14px;
|
width: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--iconColor);
|
color: var(--iconColor);
|
||||||
transition: all .25s ease-in-out;
|
transition: all .25s ease-in-out;
|
||||||
@ -52,9 +52,9 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
filter: drop-shadow(14px 0 currentColor);
|
filter: drop-shadow(16px 0 currentColor);
|
||||||
position: relative;
|
position: relative;
|
||||||
left: -14px;
|
left: -16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-front-menu > ul li > span {
|
.ag-front-menu > ul li > span {
|
||||||
|
@ -108,6 +108,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
display: block;
|
display: block;
|
||||||
color: var(--editorColor30);
|
color: var(--editorColor30);
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-image-selector span.description a {
|
.ag-image-selector span.description a {
|
||||||
@ -154,6 +155,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--editorColor);
|
color: var(--editorColor);
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-image-selector .more {
|
.ag-image-selector .more {
|
||||||
@ -161,6 +163,7 @@
|
|||||||
color: var(--editorColor);
|
color: var(--editorColor);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-image-selector .photo {
|
.ag-image-selector .photo {
|
||||||
|
@ -12,7 +12,7 @@ class ImageSelector extends BaseFloat {
|
|||||||
|
|
||||||
constructor (muya, options) {
|
constructor (muya, options) {
|
||||||
const name = 'ag-image-selector'
|
const name = 'ag-image-selector'
|
||||||
const { accessKey } = options
|
const { unsplashAccessKey } = options
|
||||||
options = Object.assign(options, {
|
options = Object.assign(options, {
|
||||||
placement: 'bottom-center',
|
placement: 'bottom-center',
|
||||||
modifiers: {
|
modifiers: {
|
||||||
@ -26,9 +26,13 @@ class ImageSelector extends BaseFloat {
|
|||||||
this.renderArray = []
|
this.renderArray = []
|
||||||
this.oldVnode = null
|
this.oldVnode = null
|
||||||
this.imageInfo = null
|
this.imageInfo = null
|
||||||
|
if (!unsplashAccessKey) {
|
||||||
|
this.unsplash = null
|
||||||
|
} else {
|
||||||
this.unsplash = new Unsplash({
|
this.unsplash = new Unsplash({
|
||||||
accessKey
|
accessKey: unsplashAccessKey
|
||||||
})
|
})
|
||||||
|
}
|
||||||
this.photoList = []
|
this.photoList = []
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.tab = 'link' // select or link
|
this.tab = 'link' // select or link
|
||||||
@ -56,7 +60,9 @@ class ImageSelector extends BaseFloat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(this.state, imageInfo.token.attrs)
|
Object.assign(this.state, imageInfo.token.attrs)
|
||||||
// load latest unsplash photos.
|
|
||||||
|
if (this.unsplash) {
|
||||||
|
// Load latest unsplash photos.
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.unsplash.photos.listPhotos(1, 40, 'latest')
|
this.unsplash.photos.listPhotos(1, 40, 'latest')
|
||||||
.then(toJson)
|
.then(toJson)
|
||||||
@ -69,9 +75,12 @@ class ImageSelector extends BaseFloat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.imageInfo = imageInfo
|
this.imageInfo = imageInfo
|
||||||
this.show(reference, cb)
|
this.show(reference, cb)
|
||||||
this.render()
|
this.render()
|
||||||
|
|
||||||
// Auto focus and select all content of the `src.input` element.
|
// Auto focus and select all content of the `src.input` element.
|
||||||
const input = this.imageSelectorContainer.querySelector('input.src')
|
const input = this.imageSelectorContainer.querySelector('input.src')
|
||||||
if (input) {
|
if (input) {
|
||||||
@ -85,6 +94,10 @@ class ImageSelector extends BaseFloat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchPhotos = (keyword) => {
|
searchPhotos = (keyword) => {
|
||||||
|
if (!this.unsplash) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.photoList = []
|
this.photoList = []
|
||||||
this.unsplash.search.photos(keyword, 1, 40)
|
this.unsplash.search.photos(keyword, 1, 40)
|
||||||
@ -253,10 +266,14 @@ class ImageSelector extends BaseFloat {
|
|||||||
}, {
|
}, {
|
||||||
label: 'Embed link',
|
label: 'Embed link',
|
||||||
value: 'link'
|
value: 'link'
|
||||||
}, {
|
}]
|
||||||
|
|
||||||
|
if (this.unsplash) {
|
||||||
|
tabs.push({
|
||||||
label: 'Unsplash',
|
label: 'Unsplash',
|
||||||
value: 'unsplash'
|
value: 'unsplash'
|
||||||
}]
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const children = tabs.map(tab => {
|
const children = tabs.map(tab => {
|
||||||
const itemSelector = this.tab === tab.value ? 'li.active' : 'li'
|
const itemSelector = this.tab === tab.value ? 'li.active' : 'li'
|
||||||
@ -285,7 +302,7 @@ class ImageSelector extends BaseFloat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 'Choose an Image'),
|
}, 'Choose an Image'),
|
||||||
h('span.description', 'Choose image from you computer.')
|
h('span.description', 'Choose image from your computer.')
|
||||||
]
|
]
|
||||||
} else if (tab === 'link') {
|
} else if (tab === 'link') {
|
||||||
const altInput = h('input.alt', {
|
const altInput = h('input.alt', {
|
||||||
@ -355,14 +372,14 @@ class ImageSelector extends BaseFloat {
|
|||||||
}
|
}
|
||||||
}, 'Embed Image')
|
}, 'Embed Image')
|
||||||
const bottomDes = h('span.description', [
|
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', {
|
h('a', {
|
||||||
on: {
|
on: {
|
||||||
click: event => {
|
click: event => {
|
||||||
this.toggleMode()
|
this.toggleMode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, `${isFullMode ? 'simple mode' : 'full mode'}`)
|
}, `${isFullMode ? 'simple mode' : 'full mode'}.`)
|
||||||
])
|
])
|
||||||
bodyContent = [inputWrapper, embedButton, bottomDes]
|
bodyContent = [inputWrapper, embedButton, bottomDes]
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,6 +3,7 @@ import Prism from 'prismjs'
|
|||||||
import katex from 'katex'
|
import katex from 'katex'
|
||||||
import loadRenderer from '../renderers'
|
import loadRenderer from '../renderers'
|
||||||
import githubMarkdownCss from 'github-markdown-css/github-markdown.css'
|
import githubMarkdownCss from 'github-markdown-css/github-markdown.css'
|
||||||
|
import footnoteCss from '../assets/styles/exportStyle.css'
|
||||||
import highlightCss from 'prismjs/themes/prism.css'
|
import highlightCss from 'prismjs/themes/prism.css'
|
||||||
import katexCss from 'katex/dist/katex.css'
|
import katexCss from 'katex/dist/katex.css'
|
||||||
import footerHeaderCss from '../assets/styles/headerFooterStyle.css'
|
import footerHeaderCss from '../assets/styles/headerFooterStyle.css'
|
||||||
@ -106,6 +107,7 @@ class ExportHtml {
|
|||||||
this.mathRendererCalled = false
|
this.mathRendererCalled = false
|
||||||
let html = marked(this.markdown, {
|
let html = marked(this.markdown, {
|
||||||
superSubScript: this.muya ? this.muya.options.superSubScript : false,
|
superSubScript: this.muya ? this.muya.options.superSubScript : false,
|
||||||
|
footnote: this.muya ? this.muya.options.footnote : false,
|
||||||
highlight (code, lang) {
|
highlight (code, lang) {
|
||||||
// Language may be undefined (GH#591)
|
// Language may be undefined (GH#591)
|
||||||
if (!lang) {
|
if (!lang) {
|
||||||
@ -247,6 +249,7 @@ class ExportHtml {
|
|||||||
list-style-type: decimal;
|
list-style-type: decimal;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<style>${footnoteCss}</style>
|
||||||
<style>${extraCss}</style>
|
<style>${extraCss}</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -4,8 +4,9 @@
|
|||||||
* Before you edit or update codes in this file,
|
* Before you edit or update codes in this file,
|
||||||
* make sure you have read this bellow:
|
* make sure you have read this bellow:
|
||||||
* Commonmark Spec: https://spec.commonmark.org/0.29/
|
* Commonmark Spec: https://spec.commonmark.org/0.29/
|
||||||
* and GitHub Flavored Markdown Spec: https://github.github.com/gfm/
|
* GitHub Flavored Markdown Spec: https://github.github.com/gfm/
|
||||||
* The output markdown needs to obey the standards of the two Spec.
|
* Pandoc Markdown: https://pandoc.org/MANUAL.html#pandocs-markdown
|
||||||
|
* The output markdown needs to obey the standards of these Spec.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class ExportMarkdown {
|
class ExportMarkdown {
|
||||||
@ -74,6 +75,10 @@ class ExportMarkdown {
|
|||||||
result.push(this.normalizeHTML(block, indent))
|
result.push(this.normalizeHTML(block, indent))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 'footnote': {
|
||||||
|
result.push(this.normalizeFootnote(block, indent))
|
||||||
|
break
|
||||||
|
}
|
||||||
case 'multiplemath': {
|
case 'multiplemath': {
|
||||||
result.push(this.normalizeMultipleMath(block, indent))
|
result.push(this.normalizeMultipleMath(block, indent))
|
||||||
break
|
break
|
||||||
@ -387,6 +392,24 @@ class ExportMarkdown {
|
|||||||
result.push(this.translateBlocks2Markdown(children, newIndent, listIndent).substring(newIndent.length))
|
result.push(this.translateBlocks2Markdown(children, newIndent, listIndent).substring(newIndent.length))
|
||||||
return result.join('')
|
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
|
export default ExportMarkdown
|
||||||
|
@ -77,8 +77,9 @@ const importRegister = ContentState => {
|
|||||||
nextSibling: null,
|
nextSibling: null,
|
||||||
children: []
|
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 token
|
||||||
let block
|
let block
|
||||||
let value
|
let value
|
||||||
@ -320,6 +321,23 @@ const importRegister = ContentState => {
|
|||||||
parentList.shift()
|
parentList.shift()
|
||||||
break
|
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': {
|
case 'list_start': {
|
||||||
const { ordered, listType, start } = token
|
const { ordered, listType, start } = token
|
||||||
block = this.createBlock(ordered === true ? 'ol' : 'ul')
|
block = this.createBlock(ordered === true ? 'ol' : 'ul')
|
||||||
@ -555,7 +573,6 @@ const importRegister = ContentState => {
|
|||||||
results.add(attrs.src)
|
results.add(attrs.src)
|
||||||
} else {
|
} else {
|
||||||
const rawSrc = label + backlash.second
|
const rawSrc = label + backlash.second
|
||||||
console.log(render.labels)
|
|
||||||
if (render.labels.has((rawSrc).toLowerCase())) {
|
if (render.labels.has((rawSrc).toLowerCase())) {
|
||||||
const { href } = render.labels.get(rawSrc.toLowerCase())
|
const { href } = render.labels.get(rawSrc.toLowerCase())
|
||||||
const { src } = getImageInfo(href)
|
const { src } = getImageInfo(href)
|
||||||
|
@ -387,3 +387,15 @@ export const verticalPositionInRect = (event, rect) => {
|
|||||||
const { top, height } = rect
|
const { top, height } = rect
|
||||||
return (clientY - top) > (height / 2) ? 'down' : 'up'
|
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-left-color: transparent;
|
||||||
border-right-color: transparent;
|
border-right-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
} /* end not print */
|
} /* end not print */
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
--iconColor: #6B737B;
|
--iconColor: #6B737B;
|
||||||
--codeBgColor: #d8d8d869;
|
--codeBgColor: #d8d8d869;
|
||||||
--codeBlockBgColor: rgba(0, 0, 0, 0.03);
|
--codeBlockBgColor: rgba(0, 0, 0, 0.03);
|
||||||
|
--footnoteBgColor: rgba(0, 0, 0, .03);
|
||||||
--inputBgColor: rgba(0, 0, 0, .06);
|
--inputBgColor: rgba(0, 0, 0, .06);
|
||||||
--focusColor: var(--themeColor);
|
--focusColor: var(--themeColor);
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
--iconColor: rgba(255, 255, 255, .56);
|
--iconColor: rgba(255, 255, 255, .56);
|
||||||
--codeBgColor: #424344;
|
--codeBgColor: #424344;
|
||||||
--codeBlockBgColor: #424344;
|
--codeBlockBgColor: #424344;
|
||||||
|
--footnoteBgColor: rgba(66, 67, 68, .3);
|
||||||
--inputBgColor: #2f3336;
|
--inputBgColor: #2f3336;
|
||||||
|
|
||||||
--focusColor: var(--themeColor);
|
--focusColor: var(--themeColor);
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
--iconColor: rgba(150, 150, 150, .8);
|
--iconColor: rgba(150, 150, 150, .8);
|
||||||
--codeBgColor: #d8d8d869;
|
--codeBgColor: #d8d8d869;
|
||||||
--codeBlockBgColor: rgba(104, 134, 170, .05);
|
--codeBlockBgColor: rgba(104, 134, 170, .05);
|
||||||
|
--footnoteBgColor: rgba(0, 0, 0, .03);
|
||||||
--inputBgColor: rgba(0, 0, 0, .06);
|
--inputBgColor: rgba(0, 0, 0, .06);
|
||||||
--focusColor: var(--themeColor);
|
--focusColor: var(--themeColor);
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
--iconColor: rgba(255, 255, 255, .56);
|
--iconColor: rgba(255, 255, 255, .56);
|
||||||
--codeBgColor: #d8d8d869;
|
--codeBgColor: #d8d8d869;
|
||||||
--codeBlockBgColor: #3f454c;
|
--codeBlockBgColor: #3f454c;
|
||||||
|
--footnoteBgColor: rgba(66, 67, 68, .5);
|
||||||
--inputBgColor: rgba(0, 0, 0, .1);
|
--inputBgColor: rgba(0, 0, 0, .1);
|
||||||
--focusColor: var(--themeColor);
|
--focusColor: var(--themeColor);
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
--iconColor: rgba(255, 255, 255, .56);
|
--iconColor: rgba(255, 255, 255, .56);
|
||||||
--codeBgColor: #3a3f4b;
|
--codeBgColor: #3a3f4b;
|
||||||
--codeBlockBgColor: #3a3f4b;
|
--codeBlockBgColor: #3a3f4b;
|
||||||
|
--footnoteBgColor: rgba(66, 67, 68, .5);
|
||||||
--inputBgColor: rgba(0, 0, 0, .1);
|
--inputBgColor: rgba(0, 0, 0, .1);
|
||||||
--focusColor: #568af2;
|
--focusColor: #568af2;
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
--iconColor: rgba(101, 101, 101, .8);
|
--iconColor: rgba(101, 101, 101, .8);
|
||||||
--codeBgColor: #d8d8d869;
|
--codeBgColor: #d8d8d869;
|
||||||
--codeBlockBgColor: rgba(12, 139, 186, .05);
|
--codeBlockBgColor: rgba(12, 139, 186, .05);
|
||||||
|
--footnoteBgColor: rgba(0, 0, 0, .03);
|
||||||
--inputBgColor: rgba(0, 0, 0, .06);
|
--inputBgColor: rgba(0, 0, 0, .06);
|
||||||
--focusColor: var(--themeColor);
|
--focusColor: var(--themeColor);
|
||||||
|
|
||||||
|
@ -88,6 +88,7 @@ import ImageToolbar from 'muya/lib/ui/imageToolbar'
|
|||||||
import Transformer from 'muya/lib/ui/transformer'
|
import Transformer from 'muya/lib/ui/transformer'
|
||||||
import FormatPicker from 'muya/lib/ui/formatPicker'
|
import FormatPicker from 'muya/lib/ui/formatPicker'
|
||||||
import LinkTools from 'muya/lib/ui/linkTools'
|
import LinkTools from 'muya/lib/ui/linkTools'
|
||||||
|
import FootnoteTool from 'muya/lib/ui/footnoteTool'
|
||||||
import TableBarTools from 'muya/lib/ui/tableTools'
|
import TableBarTools from 'muya/lib/ui/tableTools'
|
||||||
import FrontMenu from 'muya/lib/ui/frontMenu'
|
import FrontMenu from 'muya/lib/ui/frontMenu'
|
||||||
import Search from '../search'
|
import Search from '../search'
|
||||||
@ -136,6 +137,7 @@ export default {
|
|||||||
listIndentation: state => state.preferences.listIndentation,
|
listIndentation: state => state.preferences.listIndentation,
|
||||||
frontmatterType: state => state.preferences.frontmatterType,
|
frontmatterType: state => state.preferences.frontmatterType,
|
||||||
superSubScript: state => state.preferences.superSubScript,
|
superSubScript: state => state.preferences.superSubScript,
|
||||||
|
footnote: state => state.preferences.footnote,
|
||||||
lineHeight: state => state.preferences.lineHeight,
|
lineHeight: state => state.preferences.lineHeight,
|
||||||
fontSize: state => state.preferences.fontSize,
|
fontSize: state => state.preferences.fontSize,
|
||||||
codeFontSize: state => state.preferences.codeFontSize,
|
codeFontSize: state => state.preferences.codeFontSize,
|
||||||
@ -251,6 +253,12 @@ export default {
|
|||||||
editor.setOptions({ superSubScript: value }, true)
|
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) {
|
hideQuickInsertHint: function (value, oldValue) {
|
||||||
const { editor } = this
|
const { editor } = this
|
||||||
if (value !== oldValue && editor) {
|
if (value !== oldValue && editor) {
|
||||||
@ -454,6 +462,7 @@ export default {
|
|||||||
listIndentation,
|
listIndentation,
|
||||||
frontmatterType,
|
frontmatterType,
|
||||||
superSubScript,
|
superSubScript,
|
||||||
|
footnote,
|
||||||
hideQuickInsertHint,
|
hideQuickInsertHint,
|
||||||
editorLineWidth,
|
editorLineWidth,
|
||||||
theme,
|
theme,
|
||||||
@ -468,7 +477,7 @@ export default {
|
|||||||
Muya.use(EmojiPicker)
|
Muya.use(EmojiPicker)
|
||||||
Muya.use(ImagePathPicker)
|
Muya.use(ImagePathPicker)
|
||||||
Muya.use(ImageSelector, {
|
Muya.use(ImageSelector, {
|
||||||
accessKey: process.env.UNSPLASH_ACCESS_KEY,
|
unsplashAccessKey: process.env.UNSPLASH_ACCESS_KEY,
|
||||||
photoCreatorClick: this.photoCreatorClick
|
photoCreatorClick: this.photoCreatorClick
|
||||||
})
|
})
|
||||||
Muya.use(Transformer)
|
Muya.use(Transformer)
|
||||||
@ -478,6 +487,7 @@ export default {
|
|||||||
Muya.use(LinkTools, {
|
Muya.use(LinkTools, {
|
||||||
jumpClick: this.jumpClick
|
jumpClick: this.jumpClick
|
||||||
})
|
})
|
||||||
|
Muya.use(FootnoteTool)
|
||||||
Muya.use(TableBarTools)
|
Muya.use(TableBarTools)
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
@ -497,6 +507,7 @@ export default {
|
|||||||
listIndentation,
|
listIndentation,
|
||||||
frontmatterType,
|
frontmatterType,
|
||||||
superSubScript,
|
superSubScript,
|
||||||
|
footnote,
|
||||||
hideQuickInsertHint,
|
hideQuickInsertHint,
|
||||||
hideLinkPopup,
|
hideLinkPopup,
|
||||||
spellcheckEnabled: spellcheckerEnabled,
|
spellcheckEnabled: spellcheckerEnabled,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { remote } from 'electron'
|
import { remote } from 'electron'
|
||||||
import log from 'electron-log'
|
import log from 'electron-log'
|
||||||
import bus from '@/bus'
|
import bus from '@/bus'
|
||||||
|
import { getLanguageName } from '@/spellchecker/languageMap'
|
||||||
import { SEPARATOR } from './menuItems'
|
import { SEPARATOR } from './menuItems'
|
||||||
|
|
||||||
const { MenuItem } = remote
|
const { MenuItem } = remote
|
||||||
@ -24,7 +25,7 @@ export default (spellchecker, selectedWord, wordSuggestions, replaceCallback) =>
|
|||||||
const availableDictionariesSubmenu = []
|
const availableDictionariesSubmenu = []
|
||||||
for (const dict of availableDictionaries) {
|
for (const dict of availableDictionaries) {
|
||||||
availableDictionariesSubmenu.push(new MenuItem({
|
availableDictionariesSubmenu.push(new MenuItem({
|
||||||
label: dict,
|
label: getLanguageName(dict),
|
||||||
enabled: dict !== currentLanguage,
|
enabled: dict !== currentLanguage,
|
||||||
click () {
|
click () {
|
||||||
bus.$emit('switch-spellchecker-language', dict)
|
bus.$emit('switch-spellchecker-language', dict)
|
||||||
|
@ -146,6 +146,7 @@ export default {
|
|||||||
dispatch('LINTEN_FOR_PRINT_SERVICE_CLEARUP')
|
dispatch('LINTEN_FOR_PRINT_SERVICE_CLEARUP')
|
||||||
dispatch('LINTEN_FOR_EXPORT_SUCCESS')
|
dispatch('LINTEN_FOR_EXPORT_SUCCESS')
|
||||||
dispatch('LISTEN_FOR_FILE_CHANGE')
|
dispatch('LISTEN_FOR_FILE_CHANGE')
|
||||||
|
dispatch('LISTEN_WINDOW_ZOOM')
|
||||||
// module: notification
|
// module: notification
|
||||||
dispatch('LISTEN_FOR_NOTIFICATION')
|
dispatch('LISTEN_FOR_NOTIFICATION')
|
||||||
|
|
||||||
|
@ -54,6 +54,12 @@
|
|||||||
:onChange="value => onSelectChange('superSubScript', value)"
|
:onChange="value => onSelectChange('superSubScript', value)"
|
||||||
more="https://pandoc.org/MANUAL.html#superscripts-and-subscripts"
|
more="https://pandoc.org/MANUAL.html#superscripts-and-subscripts"
|
||||||
></bool>
|
></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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -95,7 +101,8 @@ export default {
|
|||||||
tabSize: state => state.preferences.tabSize,
|
tabSize: state => state.preferences.tabSize,
|
||||||
listIndentation: state => state.preferences.listIndentation,
|
listIndentation: state => state.preferences.listIndentation,
|
||||||
frontmatterType: state => state.preferences.frontmatterType,
|
frontmatterType: state => state.preferences.frontmatterType,
|
||||||
superSubScript: state => state.preferences.superSubScript
|
superSubScript: state => state.preferences.superSubScript,
|
||||||
|
footnote: state => state.preferences.footnote
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
></bool>
|
></bool>
|
||||||
<separator></separator>
|
<separator></separator>
|
||||||
<bool
|
<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"
|
:bool="spellcheckerIsHunspell"
|
||||||
:disable="!isOsx || !spellcheckerEnabled"
|
:disable="!isOsx || !spellcheckerEnabled"
|
||||||
:onChange="value => onSelectChange('spellcheckerIsHunspell', value)"
|
:onChange="value => onSelectChange('spellcheckerIsHunspell', value)"
|
||||||
@ -20,7 +20,7 @@
|
|||||||
:onChange="value => onSelectChange('spellcheckerNoUnderline', value)"
|
:onChange="value => onSelectChange('spellcheckerNoUnderline', value)"
|
||||||
></bool>
|
></bool>
|
||||||
<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"
|
:bool="spellcheckerAutoDetectLanguage"
|
||||||
:disable="!spellcheckerEnabled"
|
:disable="!spellcheckerEnabled"
|
||||||
:onChange="value => onSelectChange('spellcheckerAutoDetectLanguage', value)"
|
:onChange="value => onSelectChange('spellcheckerAutoDetectLanguage', value)"
|
||||||
@ -40,7 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="isHunspellSelected && spellcheckerEnabled">
|
<div v-if="isHunspellSelected && spellcheckerEnabled">
|
||||||
<separator></separator>
|
<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
|
<el-table
|
||||||
:data="availableDictionaries"
|
:data="availableDictionaries"
|
||||||
style="width: 100%">
|
style="width: 100%">
|
||||||
@ -65,7 +65,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="description">Add new dictionaries to Hunspell.</div>
|
<div class="description">Download new dictionaries for Hunspell.</div>
|
||||||
<div class="dictionary-group">
|
<div class="dictionary-group">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="selectedDictionaryToAdd"
|
v-model="selectedDictionaryToAdd"
|
||||||
@ -210,8 +210,13 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
startDownloadHunspellDictionary (languageCode) {
|
startDownloadHunspellDictionary (languageCode) {
|
||||||
|
this.errorMessage = ''
|
||||||
if (this.hunspellDictionaryDownloadCache[languageCode]) {
|
if (this.hunspellDictionaryDownloadCache[languageCode]) {
|
||||||
return
|
return
|
||||||
|
} else if (!navigator.onLine) {
|
||||||
|
delete this.hunspellDictionaryDownloadCache[languageCode]
|
||||||
|
this.errorMessage = 'No Internet connection available.'
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hunspellDictionaryDownloadCache[languageCode] = 1
|
this.hunspellDictionaryDownloadCache[languageCode] = 1
|
||||||
|
@ -18,3 +18,14 @@ export const themes = [
|
|||||||
name: 'one-dark'
|
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">
|
<section class="offcial-themes">
|
||||||
<div v-for="t of themes" :key="t.name" class="theme"
|
<div v-for="t of themes" :key="t.name" class="theme"
|
||||||
:class="[t.name, { 'active': t.name === theme }]"
|
:class="[t.name, { 'active': t.name === theme }]"
|
||||||
@click="handleSelectTheme(t.name)"
|
@click="onSelectChange('theme', t.name)"
|
||||||
>
|
>
|
||||||
<div v-html="t.html"></div>
|
<div v-html="t.html"></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<separator></separator>
|
<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">
|
<section class="import-themes ag-underdevelop">
|
||||||
<div>
|
<div>
|
||||||
<span>Open the themes folder</span>
|
<span>Open the themes folder</span>
|
||||||
@ -27,21 +34,25 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import themeMd from './theme.md'
|
import themeMd from './theme.md'
|
||||||
import { themes } from './config'
|
import { autoSwitchThemeOptions, themes } from './config'
|
||||||
import markdownToHtml from '@/util/markdownToHtml'
|
import markdownToHtml from '@/util/markdownToHtml'
|
||||||
|
import CurSelect from '../common/select'
|
||||||
import Separator from '../common/separator'
|
import Separator from '../common/separator'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
CurSelect,
|
||||||
Separator
|
Separator
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
|
this.autoSwitchThemeOptions = autoSwitchThemeOptions
|
||||||
return {
|
return {
|
||||||
themes: []
|
themes: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
|
autoSwitchTheme: state => state.preferences.autoSwitchTheme,
|
||||||
theme: state => state.preferences.theme
|
theme: state => state.preferences.theme
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -60,11 +71,8 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleSelectTheme (theme) {
|
onSelectChange (type, value) {
|
||||||
this.$store.dispatch('SET_SINGLE_PREFERENCE', {
|
this.$store.dispatch('SET_SINGLE_PREFERENCE', { type, value })
|
||||||
type: 'theme',
|
|
||||||
value: theme
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,7 +92,7 @@ export default {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 250px;
|
width: 250px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
margin: 0px 22px 10px 22px;
|
margin: 0px 20px 10px 20px;
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -17,11 +17,26 @@ export const downloadHunspellDictionary = async lang => {
|
|||||||
responseType: 'stream'
|
responseType: 'stream'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const dstFile = path.join(dictionaryPath, `${lang}.bdic`)
|
||||||
|
const tmpFile = `${dstFile}.tmp`
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const outStream = fs.createWriteStream(path.join(dictionaryPath, `${lang}.bdic`))
|
const outStream = fs.createWriteStream(tmpFile)
|
||||||
response.data.pipe(outStream)
|
response.data.pipe(outStream)
|
||||||
|
|
||||||
|
let totalLength = 0
|
||||||
|
response.data.on('data', chunk => {
|
||||||
|
totalLength += chunk.length
|
||||||
|
})
|
||||||
|
|
||||||
outStream.once('error', reject)
|
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.
|
* Returns true if not misspelled words should be highlighted.
|
||||||
*/
|
*/
|
||||||
get spellcheckerNoUnderline () {
|
get isPassiveMode () {
|
||||||
if (!this.isEnabled) {
|
if (!this.isEnabled) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return this.provider.spellcheckerNoUnderline
|
return this.provider.isPassiveMode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should we highlight misspelled words.
|
* Should we highlight misspelled words.
|
||||||
*/
|
*/
|
||||||
set spellcheckerNoUnderline (value) {
|
set isPassiveMode (value) {
|
||||||
if (!this.isEnabled) {
|
if (!this.isEnabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.provider.spellcheckerNoUnderline = !!value
|
this.provider.isPassiveMode = !!value
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,8 +42,10 @@ const state = {
|
|||||||
listIndentation: 1,
|
listIndentation: 1,
|
||||||
frontmatterType: '-',
|
frontmatterType: '-',
|
||||||
superSubScript: false,
|
superSubScript: false,
|
||||||
|
footnote: false,
|
||||||
|
|
||||||
theme: 'light',
|
theme: 'light',
|
||||||
|
autoSwitchTheme: 2,
|
||||||
|
|
||||||
spellcheckerEnabled: false,
|
spellcheckerEnabled: false,
|
||||||
spellcheckerIsHunspell: false, // macOS only
|
spellcheckerIsHunspell: false, // macOS only
|
||||||
|
@ -39,8 +39,10 @@
|
|||||||
"listIndentation": 1,
|
"listIndentation": 1,
|
||||||
"frontmatterType": "-",
|
"frontmatterType": "-",
|
||||||
"superSubScript": false,
|
"superSubScript": false,
|
||||||
|
"footnote": false,
|
||||||
|
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
|
"autoSwitchTheme": 2,
|
||||||
|
|
||||||
"spellcheckerEnabled": false,
|
"spellcheckerEnabled": false,
|
||||||
"spellcheckerIsHunspell": false,
|
"spellcheckerIsHunspell": false,
|
||||||
|