fix conflict

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

View File

@ -51,7 +51,7 @@ const rendererConfig = {
} }
}, },
{ {
test: /(theme\-chalk(?:\/|\\)index|katex|github\-markdown|prism[\-a-z]*|\.theme|headerFooterStyle)\.css$/, test: /(theme\-chalk(?:\/|\\)index|exportStyle|katex|github\-markdown|prism[\-a-z]*|\.theme|headerFooterStyle)\.css$/,
use: [ 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 } },

View File

@ -51,6 +51,7 @@ Preferences can be controlled and modified in the settings window or via the `pr
| listIndentation | String | 1 | The list indentation of sub list items or paragraphs, optional value `dfm`, `tab` or number 1~4 | | 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -60,9 +60,7 @@ try {
// Catch errors that may come from invalid configuration files like settings. // 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,24 +3,12 @@ import fs from 'fs'
import path from 'path' import 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) {

View File

@ -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.",

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

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

View File

@ -180,6 +180,55 @@ figure[data-role="HTML"].ag-active .ag-html-preview {
display: none; 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;
}

View File

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

View File

@ -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()
} }
} }
} }

View File

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

View File

@ -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()
} }
} }

View File

@ -0,0 +1,63 @@
/* eslint-disable no-useless-escape */
const FOOTNOTE_REG = /^\[\^([^\^\[\]\s]+?)(?<!\\)\]: /
/* eslint-enable no-useless-escape */
const footnoteCtrl = ContentState => {
ContentState.prototype.updateFootnote = function (block, line) {
const { start, end } = this.cursor
const { text } = line
const match = FOOTNOTE_REG.exec(text)
const footnoteIdentifer = match[1]
const sectionWrapper = this.createBlock('figure', {
functionType: 'footnote'
})
const footnoteInput = this.createBlock('span', {
text: footnoteIdentifer,
functionType: 'footnoteInput'
})
const pBlock = this.createBlockP(text.substring(match[0].length))
this.appendChild(sectionWrapper, footnoteInput)
this.appendChild(sectionWrapper, pBlock)
this.insertBefore(sectionWrapper, block)
this.removeBlock(block)
const { key } = pBlock.children[0]
this.cursor = {
start: {
key,
offset: Math.max(0, start.offset - footnoteIdentifer.length)
},
end: {
key,
offset: Math.max(0, end.offset - footnoteIdentifer.length)
}
}
if (this.isCollapse()) {
this.checkInlineUpdate(pBlock.children[0])
}
this.render()
return sectionWrapper
}
ContentState.prototype.createFootnote = function (identifier) {
const { blocks } = this
const lastBlock = blocks[blocks.length - 1]
const newBlock = this.createBlockP(`[^${identifier}]: `)
this.insertAfter(newBlock, lastBlock)
const key = newBlock.children[0].key
const offset = newBlock.children[0].text.length
this.cursor = {
start: { key, offset },
end: { key, offset }
}
const sectionWrapper = this.updateFootnote(newBlock, newBlock.children[0])
const id = sectionWrapper.key
const footnoteEle = document.querySelector(`#${id}`)
if (footnoteEle) {
footnoteEle.scrollIntoView({ behavior: 'smooth' })
}
}
}
export default footnoteCtrl

View File

@ -86,7 +86,12 @@ const clearFormat = (token, { start, end }) => {
} }
const addFormat = (type, block, { start, end }) => { 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':

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,12 +32,12 @@ const correctUrl = token => {
} }
} }
const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => { const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels, options) => {
const originSrc = src const 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) {

View File

@ -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*\])(?:\\[\[\]]|[^\[\]])+/

View File

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

View File

@ -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.!#$%&'*+\/=?_`{\|}~-]+@))/,
// ------------------------ // ------------------------

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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())
} }
} }

View File

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

View File

@ -21,6 +21,7 @@ import flowchartIcon from '../../../assets/pngicon/flowchart/2.png'
import sequenceIcon from '../../../assets/pngicon/sequence/2.png' import 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) {

View File

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

View File

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

View File

@ -28,6 +28,7 @@ import htmlRuby from './htmlRuby'
import referenceLink from './referenceLink' import 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
} }

View File

@ -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 */

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import imageIcon from '../../assets/pngicon/format_image/2.png'
import linkIcon from '../../assets/pngicon/format_link/2.png' import 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',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
})
}) })
} }

View File

@ -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
} }
/** /**

View File

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

View File

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

1429
yarn.lock

File diff suppressed because it is too large Load Diff