diff --git a/.electron-vue/webpack.renderer.config.js b/.electron-vue/webpack.renderer.config.js
index 96ffeb75..300113e1 100644
--- a/.electron-vue/webpack.renderer.config.js
+++ b/.electron-vue/webpack.renderer.config.js
@@ -51,7 +51,7 @@ const rendererConfig = {
}
},
{
- test: /(theme\-chalk(?:\/|\\)index|katex|github\-markdown|prism[\-a-z]*|\.theme|headerFooterStyle)\.css$/,
+ test: /(theme\-chalk(?:\/|\\)index|exportStyle|katex|github\-markdown|prism[\-a-z]*|\.theme|headerFooterStyle)\.css$/,
use: [
'to-string-loader',
'css-loader'
@@ -59,7 +59,7 @@ const rendererConfig = {
},
{
test: /\.css$/,
- exclude: /(theme\-chalk(?:\/|\\)index|katex|github\-markdown|prism[\-a-z]*|\.theme|headerFooterStyle)\.css$/,
+ exclude: /(theme\-chalk(?:\/|\\)index|exportStyle|katex|github\-markdown|prism[\-a-z]*|\.theme|headerFooterStyle)\.css$/,
use: [
proMode ? MiniCssExtractPlugin.loader : 'style-loader',
{ loader: 'css-loader', options: { importLoaders: 1 } },
diff --git a/docs/PREFERENCES.md b/docs/PREFERENCES.md
index 848b122f..0f3a60e7 100644
--- a/docs/PREFERENCES.md
+++ b/docs/PREFERENCES.md
@@ -51,6 +51,7 @@ Preferences can be controlled and modified in the settings window or via the `pr
| listIndentation | String | 1 | The list indentation of sub list items or paragraphs, optional value `dfm`, `tab` or number 1~4 |
| frontmatterType | String | `-` | The frontmatter type: `-` (YAML), `+` (TOML), `;` (JSON) or `{` (JSON) |
| superSubScript | Boolean | `false` | Enable pandoc's markdown extension superscript and subscript. |
+| footnote | Boolean | `false` | Enable pandoc's footnote markdown extension |
#### Theme
diff --git a/docs/dev/code/BLOCK_ADDITION_PROPERTY.md b/docs/dev/code/BLOCK_ADDITION_PROPERTY.md
index 7fa03ee3..06671f2a 100644
--- a/docs/dev/code/BLOCK_ADDITION_PROPERTY.md
+++ b/docs/dev/code/BLOCK_ADDITION_PROPERTY.md
@@ -6,6 +6,8 @@
- languageInput
+ - footnoteInput
+
- codeContent (used in code block)
- cellContent (used in table cell, it's parent must be th or td block)
@@ -46,6 +48,8 @@ The container block of `table`, `html`, `block math`, `mermaid`,`flowchart`,`veg
- table
+ - footnote
+
- html
- multiplemath
diff --git a/electron-builder.yml b/electron-builder.yml
index 12b264c6..8d1a53bb 100755
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -19,6 +19,7 @@ files:
- "!node_modules/vega-lite/build/vega-lite*.js.map"
# Don't bundle build files
- "!node_modules/@felixrieseberg/spellchecker/bin"
+- "!node_modules/@hfelix/spellchecker/bin"
- "!node_modules/ced/bin"
- "!node_modules/ced/vendor"
- "!node_modules/cld/bin"
@@ -34,6 +35,7 @@ files:
- "!node_modules/ced/build/vendor"
# Don't bundle LGPL source files
- "!node_modules/@felixrieseberg/spellchecker/vendor"
+- "!node_modules/@hfelix/spellchecker/vendor"
extraFiles:
- "LICENSE"
- from: "resources/THIRD-PARTY-LICENSES.txt"
diff --git a/package.json b/package.json
index ceabf11d..a1c7c10a 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
},
"dependencies": {
"@hfelix/electron-localshortcut": "^3.1.1",
- "@hfelix/electron-spellchecker": "^1.0.0-rc.1",
+ "@hfelix/electron-spellchecker": "^1.0.0-rc.3",
"@octokit/rest": "^16.33.1",
"arg": "^4.1.1",
"axios": "^0.19.0",
@@ -65,7 +65,7 @@
"joplin-turndown-plugin-gfm": "^1.0.11",
"katex": "^0.11.1",
"keyboard-layout": "^2.0.16",
- "keytar": "^5.0.0-beta.3",
+ "keytar": "5.0.0-beta.4",
"mermaid": "^8.4.0",
"plist": "^3.0.1",
"popper.js": "^1.16.0",
@@ -113,8 +113,8 @@
"del": "^5.1.0",
"devtron": "^1.4.0",
"dotenv": "^8.2.0",
- "electron": "^6.1.0",
- "electron-builder": "^21.2.0",
+ "electron": "7.0.0",
+ "electron-builder": "^22.1.0",
"electron-devtools-installer": "^2.2.4",
"electron-rebuild": "^1.8.6",
"electron-updater": "^4.1.2",
@@ -153,7 +153,7 @@
"postcss-preset-env": "^6.6.0",
"raw-loader": "^3.1.0",
"require-dir": "^1.2.0",
- "spectron": "^8.0.0",
+ "spectron": "^9.0.0",
"style-loader": "^1.0.0",
"svg-sprite-loader": "^4.1.6",
"svgo": "^1.3.0",
@@ -171,9 +171,6 @@
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^4.2.1"
},
- "optionalDependencies": {
- "vscode-windows-registry": "^1.0.2"
- },
"repository": {
"type": "git",
"url": "git@github.com:marktext/marktext.git"
diff --git a/src/main/app/index.js b/src/main/app/index.js
index 2954984d..981faa95 100644
--- a/src/main/app/index.js
+++ b/src/main/app/index.js
@@ -3,7 +3,7 @@ import fse from 'fs-extra'
import { exec } from 'child_process'
import dayjs from 'dayjs'
import log from 'electron-log'
-import { app, BrowserWindow, clipboard, dialog, ipcMain, systemPreferences } from 'electron'
+import { app, BrowserWindow, clipboard, dialog, ipcMain, nativeTheme } from 'electron'
import { isChildOfDirectory } from 'common/filesystem/paths'
import { isLinux, isOsx, isWindows } from '../config'
import parseArgs from '../cli/parser'
@@ -115,7 +115,7 @@ class App {
const { paths } = this._accessor
ensureDefaultDict(paths.userDataPath)
.catch(error => {
- log.error(error)
+ log.error('Error copying Hunspell dictionary: ', error)
})
}
@@ -143,7 +143,13 @@ class App {
}
}
- const { startUpAction, defaultDirectoryToOpen } = preferences.getAll()
+ const {
+ startUpAction,
+ defaultDirectoryToOpen,
+ autoSwitchTheme,
+ theme
+ } = preferences.getAll()
+
if (startUpAction === 'folder' && defaultDirectoryToOpen) {
const info = normalizeMarkdownPath(defaultDirectoryToOpen)
if (info) {
@@ -151,29 +157,32 @@ class App {
}
}
+ // Set initial native theme for theme in preferences.
+ const isDarkTheme = /dark/i.test(theme)
+ if (autoSwitchTheme === 0 && isDarkTheme !== nativeTheme.shouldUseDarkColors) {
+ selectTheme(nativeTheme.shouldUseDarkColors ? 'dark' : 'light')
+ nativeTheme.themeSource = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'
+ } else {
+ nativeTheme.themeSource = isDarkTheme ? 'dark' : 'light'
+ }
+
+ let isDarkMode = nativeTheme.shouldUseDarkColors
+ ipcMain.on('broadcast-preferences-changed', change => {
+ // Set Chromium's color for native elements after theme change.
+ if (change.theme) {
+ const isDarkTheme = /dark/i.test(change.theme)
+ if (isDarkMode !== isDarkTheme) {
+ isDarkMode = isDarkTheme
+ nativeTheme.themeSource = isDarkTheme ? 'dark' : 'light'
+ } else if (nativeTheme.themeSource === 'system') {
+ // Need to set dark or light theme because we set `system` to get the current system theme.
+ nativeTheme.themeSource = isDarkMode ? 'dark' : 'light'
+ }
+ }
+ })
+
if (isOsx) {
app.dock.setMenu(dockMenu)
-
- // Listen for system theme change and change Mark Text own `dark` and `light`.
- // In macOS 10.14 Mojave, Apple introduced a new system-wide dark mode for
- // all macOS computers.
- systemPreferences.subscribeNotification(
- 'AppleInterfaceThemeChangedNotification',
- () => {
- const preferences = this._accessor.preferences
- const { theme } = preferences.getAll()
-
- // Application menu is automatically updated via preference manager.
- if (systemPreferences.isDarkMode() && theme !== 'dark' &&
- theme !== 'material-dark' && theme !== 'one-dark') {
- selectTheme('dark')
- }
- if (!systemPreferences.isDarkMode() && theme !== 'light' &&
- theme !== 'ulysses' && theme !== 'graphite') {
- selectTheme('light')
- }
- }
- )
} else if (isWindows) {
app.setJumpList([{
type: 'recent'
diff --git a/src/main/config.js b/src/main/config.js
index f1f28771..90523f28 100644
--- a/src/main/config.js
+++ b/src/main/config.js
@@ -16,7 +16,7 @@ export const editorWinOptions = {
zoomFactor: 1.0
}
-export const defaultPreferenceWinOptions = {
+export const preferencesWinOptions = {
width: 950,
height: 650,
webPreferences: {
diff --git a/src/main/dataCenter/index.js b/src/main/dataCenter/index.js
index 9742fe38..4b59b7d9 100644
--- a/src/main/dataCenter/index.js
+++ b/src/main/dataCenter/index.js
@@ -71,7 +71,7 @@ class DataCenter extends EventEmitter {
return Object.assign(data, encryptObj)
} catch (err) {
- log.error(err)
+ log.error('Failed to decrypt secure keys:', err)
return data
}
}
@@ -133,7 +133,7 @@ class DataCenter extends EventEmitter {
try {
return await keytar.setPassword(serviceName, key, value)
} catch (err) {
- log.error(err)
+ log.error('dataCenter::setItem:', err)
}
} else {
return this.store.set(key, value)
diff --git a/src/main/filesystem/watcher.js b/src/main/filesystem/watcher.js
index fa5a3696..5d242a50 100644
--- a/src/main/filesystem/watcher.js
+++ b/src/main/filesystem/watcher.js
@@ -235,7 +235,7 @@ class Watcher {
})
}
} else {
- log.error(error)
+ log.error('Error while watching files:', error)
}
})
diff --git a/src/main/index.js b/src/main/index.js
index 19a588dd..60d39444 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -60,9 +60,7 @@ try {
// Catch errors that may come from invalid configuration files like settings.
const msgHint = err.message.includes('Config schema violation')
? 'This seems to be an issue with your configuration file(s). ' : ''
-
- log.error(`Loading Mark Text failed during initialization! ${msgHint}`)
- log.error(err)
+ log.error(`Loading Mark Text failed during initialization! ${msgHint}`, err)
const EXIT_ON_ERROR = !!process.env.MARKTEXT_EXIT_ON_ERROR
const SHOW_ERROR_DIALOG = !process.env.MARKTEXT_ERROR_INTERACTION
diff --git a/src/main/menu/actions/file.js b/src/main/menu/actions/file.js
index 4eba8183..1dccfa3d 100644
--- a/src/main/menu/actions/file.js
+++ b/src/main/menu/actions/file.js
@@ -62,7 +62,7 @@ const handleResponseForExport = async (e, { type, content, pathname, title, page
}
win.webContents.send('AGANI::export-success', { type, filePath })
} catch (err) {
- log.error(err)
+ log.error('Error while exporting:', err)
const ERROR_MSG = err.message || `Error happened when export ${filePath}`
win.webContents.send('AGANI::show-notification', {
title: 'Export failure',
@@ -80,19 +80,9 @@ const handleResponseForExport = async (e, { type, content, pathname, title, page
const handleResponseForPrint = e => {
const win = BrowserWindow.fromWebContents(e.sender)
-
- // See GH#749, Electron#16085 and Electron#17523.
- dialog.showMessageBox(win, {
- type: 'info',
- buttons: ['OK'],
- defaultId: 0,
- noLink: true,
- message: 'Printing doesn\'t work',
- detail: 'Printing is disabled due to an Electron upstream issue. Please export the document as PDF and print the PDF file. We apologize for the inconvenience!'
+ win.webContents.print({ printBackground: true }, () => {
+ removePrintServiceFromWindow(win)
})
- // win.webContents.print({ printBackground: true }, () => {
- // removePrintServiceFromWindow(win)
- // })
}
const handleResponseForSave = async (e, { id, filename, markdown, pathname, options, defaultPath }) => {
@@ -140,7 +130,7 @@ const handleResponseForSave = async (e, { id, filename, markdown, pathname, opti
return id
})
.catch(err => {
- log.error(err)
+ log.error('Error while saving:', err)
win.webContents.send('mt::tab-save-failure', id, err.message)
})
}
@@ -185,7 +175,7 @@ const openPandocFile = async (windowId, pathname) => {
const data = await converter()
ipcMain.emit('app-open-markdown-by-id', windowId, data)
} catch (err) {
- log.error(err)
+ log.error('Error while converting file:', err)
}
}
@@ -216,7 +206,7 @@ ipcMain.on('mt::save-and-close-tabs', async (e, unsavedFiles) => {
win.send('mt::force-close-tabs-by-id', tabIds)
})
.catch(err => {
- log.error(err.error)
+ log.error('Error while save all:', err.error)
})
} else {
const tabIds = unsavedFiles.map(f => f.id)
@@ -262,7 +252,7 @@ ipcMain.on('AGANI::response-file-save-as', async (e, { id, filename, markdown, p
}
})
.catch(err => {
- log.error(err)
+ log.error('Error while save as:', err)
win.webContents.send('mt::tab-save-failure', id, err.message)
})
}
@@ -282,8 +272,7 @@ ipcMain.on('mt::close-window-confirm', async (e, unsavedFiles) => {
ipcMain.emit('window-close-by-id', win.id)
})
.catch(err => {
- console.log(err)
- log.error(err)
+ log.error('Error while saving before quit:', err)
// Notify user about the problem.
dialog.showMessageBox(win, {
@@ -446,19 +435,9 @@ export const importFile = async win => {
}
export const print = win => {
- if (!win) {
- return
+ if (win) {
+ win.webContents.send('mt::show-export-dialog', 'print')
}
- // See GH#749, Electron#16085 and Electron#17523.
- dialog.showMessageBox(win, {
- type: 'info',
- buttons: ['OK'],
- defaultId: 0,
- noLink: true,
- message: 'Printing doesn\'t work',
- detail: 'Printing is disabled due to an Electron upstream issue. Please export the document as PDF and print the PDF file. We apologize for the inconvenience!'
- })
- // win.webContents.send('mt::show-export-dialog', 'print')
}
export const openFile = async win => {
diff --git a/src/main/menu/actions/window.js b/src/main/menu/actions/window.js
index 4874f605..01a0a077 100644
--- a/src/main/menu/actions/window.js
+++ b/src/main/menu/actions/window.js
@@ -9,13 +9,13 @@ export const toggleAlwaysOnTop = win => {
export const zoomIn = win => {
const { webContents } = win
const zoom = webContents.getZoomFactor()
- // WORKAROUND: Electron#16018
+ // WORKAROUND: We need to set zoom on the browser window due to Electron#16018.
webContents.send('mt::window-zoom', Math.min(2.0, zoom + 0.125))
}
export const zoomOut = win => {
const { webContents } = win
const zoom = webContents.getZoomFactor()
- // WORKAROUND: Electron#16018
+ // WORKAROUND: We need to set zoom on the browser window due to Electron#16018.
webContents.send('mt::window-zoom', Math.max(1.0, zoom - 0.125))
}
diff --git a/src/main/menu/index.js b/src/main/menu/index.js
index 0ac669fa..6f811c53 100644
--- a/src/main/menu/index.js
+++ b/src/main/menu/index.js
@@ -92,7 +92,7 @@ class AppMenu {
}
return recentDocuments
} catch (err) {
- log.error(err)
+ log.error('Error while read recently used documents:', err)
return []
}
}
diff --git a/src/main/menu/templates/paragraph.js b/src/main/menu/templates/paragraph.js
index d927de53..b6400a05 100644
--- a/src/main/menu/templates/paragraph.js
+++ b/src/main/menu/templates/paragraph.js
@@ -166,7 +166,7 @@ export default function (keybindings) {
}
}, {
id: 'frontMatterMenuItem',
- label: 'YAML Front Matter',
+ label: 'Front Matter',
type: 'checkbox',
accelerator: keybindings.getAccelerator('paragraphYAMLFrontMatter'),
click (menuItem, browserWindow) {
diff --git a/src/main/menu/templates/window.js b/src/main/menu/templates/window.js
index cc0b5e38..4e69c566 100755
--- a/src/main/menu/templates/window.js
+++ b/src/main/menu/templates/window.js
@@ -17,19 +17,13 @@ export default function (keybindings) {
toggleAlwaysOnTop(browserWindow)
}
}, {
- // TODO: Disable due GH#1225.
- visible: false,
type: 'separator'
}, {
- // TODO: Disable due GH#1225.
- visible: false,
label: 'Zoom In',
click (menuItem, browserWindow) {
zoomIn(browserWindow)
}
}, {
- // TODO: Disable due GH#1225.
- visible: false,
label: 'Zoom Out',
click (menuItem, browserWindow) {
zoomOut(browserWindow)
diff --git a/src/main/platform/win32/registry.js b/src/main/platform/win32/registry.js
deleted file mode 100755
index d027d751..00000000
--- a/src/main/platform/win32/registry.js
+++ /dev/null
@@ -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
- }
-}
diff --git a/src/main/preferences/index.js b/src/main/preferences/index.js
index 48e4fa81..69f69687 100644
--- a/src/main/preferences/index.js
+++ b/src/main/preferences/index.js
@@ -3,24 +3,12 @@ import fs from 'fs'
import path from 'path'
import EventEmitter from 'events'
import Store from 'electron-store'
-import { BrowserWindow, ipcMain, systemPreferences } from 'electron'
+import { BrowserWindow, ipcMain, nativeTheme } from 'electron'
import log from 'electron-log'
-import { isOsx, isWindows } from '../config'
+import { isWindows } from '../config'
import { hasSameKeys } from '../utils'
-import { getStringRegKey, winHKEY } from '../platform/win32/registry.js'
import schema from './schema'
-const isDarkSystemMode = () => {
- if (isOsx) {
- return systemPreferences.isDarkMode()
- } else if (isWindows) {
- // NOTE: This key is a 32-Bit DWORD but converted to JS string!
- const buf = getStringRegKey(winHKEY.HKCU, 'Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize', 'AppsUseLightTheme')
- return buf === '' // zero (0)
- }
- return false
-}
-
const PREFERENCES_FILE_NAME = 'preferences'
class Preference extends EventEmitter {
@@ -50,7 +38,9 @@ class Preference extends EventEmitter {
let defaultSettings = null
try {
defaultSettings = fse.readJsonSync(this.staticPath)
- if (isDarkSystemMode()) {
+
+ // Set best theme on first application start.
+ if (nativeTheme.shouldUseDarkColors) {
defaultSettings.theme = 'dark'
}
} catch (err) {
diff --git a/src/main/preferences/schema.json b/src/main/preferences/schema.json
index 4f026f6a..26e6cf69 100644
--- a/src/main/preferences/schema.json
+++ b/src/main/preferences/schema.json
@@ -242,11 +242,25 @@
"type": "boolean",
"default": false
},
+ "footnote": {
+ "description": "Markdown-Enable pandoc's markdown extension footnote.",
+ "type": "boolean",
+ "default": false
+ },
"theme": {
"description": "Theme--Select the theme used in Mark Text",
"type": "string"
},
+ "autoSwitchTheme": {
+ "description": "Theme--Automatically adjust application theme according system.",
+ "default": 2,
+ "enum": [
+ 0,
+ 1,
+ 2
+ ]
+ },
"spellcheckerEnabled": {
"description": "Spelling--Whether spell checking is enabled.",
diff --git a/src/main/utils/imagePathAutoComplement.js b/src/main/utils/imagePathAutoComplement.js
index e9a3d69b..235c86c6 100644
--- a/src/main/utils/imagePathAutoComplement.js
+++ b/src/main/utils/imagePathAutoComplement.js
@@ -46,8 +46,9 @@ const filesHandler = (files, directory, key) => {
const rebuild = (directory) => {
fs.readdir(directory, (err, files) => {
- if (err) log.error(err)
- else {
+ if (err) {
+ log.error('imagePathAutoComplement::rebuild:', err)
+ } else {
filesHandler(files, directory)
}
})
diff --git a/src/main/windows/setting.js b/src/main/windows/setting.js
index f0a27751..b9eb5554 100644
--- a/src/main/windows/setting.js
+++ b/src/main/windows/setting.js
@@ -3,7 +3,7 @@ import { BrowserWindow, ipcMain } from 'electron'
import electronLocalshortcut from '@hfelix/electron-localshortcut'
import BaseWindow, { WindowLifecycle, WindowType } from './base'
import { centerWindowOptions } from './utils'
-import { TITLE_BAR_HEIGHT, defaultPreferenceWinOptions, isLinux, isOsx } from '../config'
+import { TITLE_BAR_HEIGHT, preferencesWinOptions, isLinux, isOsx, isWindows } from '../config'
class SettingWindow extends BaseWindow {
/**
@@ -21,12 +21,18 @@ class SettingWindow extends BaseWindow {
*/
createWindow (options = {}) {
const { menu: appMenu, env, keybindings, preferences } = this._accessor
- const winOptions = Object.assign({}, defaultPreferenceWinOptions, options)
+ const winOptions = Object.assign({}, preferencesWinOptions, options)
centerWindowOptions(winOptions)
if (isLinux) {
winOptions.icon = path.join(__static, 'logo-96px.png')
}
+ // WORKAROUND: Electron has issues with different DPI per monitor when
+ // setting a fixed window size.
+ if (isWindows) {
+ winOptions.resizable = true
+ }
+
// Enable native or custom/frameless window and titlebar
const { titleBarStyle, theme } = preferences.getAll()
if (!isOsx) {
diff --git a/src/muya/lib/assets/pngicon/footnote/1.png b/src/muya/lib/assets/pngicon/footnote/1.png
new file mode 100755
index 00000000..379ca9a6
Binary files /dev/null and b/src/muya/lib/assets/pngicon/footnote/1.png differ
diff --git a/src/muya/lib/assets/pngicon/footnote/2.png b/src/muya/lib/assets/pngicon/footnote/2.png
new file mode 100755
index 00000000..bff14d4d
Binary files /dev/null and b/src/muya/lib/assets/pngicon/footnote/2.png differ
diff --git a/src/muya/lib/assets/pngicon/footnote/3.png b/src/muya/lib/assets/pngicon/footnote/3.png
new file mode 100755
index 00000000..1b2b84aa
Binary files /dev/null and b/src/muya/lib/assets/pngicon/footnote/3.png differ
diff --git a/src/muya/lib/assets/pngicon/highlight/1.png b/src/muya/lib/assets/pngicon/highlight/1.png
new file mode 100755
index 00000000..107c43f1
Binary files /dev/null and b/src/muya/lib/assets/pngicon/highlight/1.png differ
diff --git a/src/muya/lib/assets/pngicon/highlight/2.png b/src/muya/lib/assets/pngicon/highlight/2.png
new file mode 100755
index 00000000..eb36a37c
Binary files /dev/null and b/src/muya/lib/assets/pngicon/highlight/2.png differ
diff --git a/src/muya/lib/assets/pngicon/highlight/3.png b/src/muya/lib/assets/pngicon/highlight/3.png
new file mode 100755
index 00000000..4d160226
Binary files /dev/null and b/src/muya/lib/assets/pngicon/highlight/3.png differ
diff --git a/src/muya/lib/assets/pngicon/warning/2.png b/src/muya/lib/assets/pngicon/warning/2.png
new file mode 100644
index 00000000..8d899f6f
Binary files /dev/null and b/src/muya/lib/assets/pngicon/warning/2.png differ
diff --git a/src/muya/lib/assets/styles/exportStyle.css b/src/muya/lib/assets/styles/exportStyle.css
new file mode 100644
index 00000000..4d15756d
--- /dev/null
+++ b/src/muya/lib/assets/styles/exportStyle.css
@@ -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;
+}
diff --git a/src/muya/lib/assets/styles/index.css b/src/muya/lib/assets/styles/index.css
index ec94dfe7..c9ab6d3b 100644
--- a/src/muya/lib/assets/styles/index.css
+++ b/src/muya/lib/assets/styles/index.css
@@ -180,6 +180,55 @@ figure[data-role="HTML"].ag-active .ag-html-preview {
display: none;
}
+figure[data-role="FOOTNOTE"] {
+ position: relative;
+ background: var(--footnoteBgColor);
+ padding: 1.2em 2em .05em 1em;
+ font-size: .8em;
+ opacity: .8;
+}
+
+figure[data-role="FOOTNOTE"] > p:first-of-type .ag-paragraph-content:empty::after {
+ content: 'Input the footnote definition...';
+ color: var(--editorColor30);
+}
+
+figure[data-role="FOOTNOTE"].ag-active::before {
+ content: attr(data-role);
+ text-transform: lowercase;
+ position: absolute;
+ top: .2em;
+ right: 1em;
+ color: var(--editorColor30);
+ font-size: 12px;
+}
+
+figure[data-role="FOOTNOTE"] pre {
+ font-size: .8em;
+}
+
+figure[data-role="FOOTNOTE"] .ag-footnote-input {
+ padding: 0 1em;
+ min-width: 80px;
+ position: absolute;
+ top: 0.2em;
+ left: 0;
+ font-size: 14px;
+ font-family: monospace;
+ font-weight: 600;
+ color: var(--editorColor);
+ background: transparent;
+ z-index: 1;
+}
+
+figure[data-role="FOOTNOTE"] .ag-footnote-input::before {
+ content: '[^';
+}
+
+figure[data-role="FOOTNOTE"] .ag-footnote-input::after {
+ content: ']:';
+}
+
.ag-highlight {
animation-name: highlight;
animation-duration: .25s;
@@ -1194,3 +1243,28 @@ figure:not(.ag-active) pre.ag-paragraph.line-numbers {
top: .05em;
}
+.ag-inline-footnote-identifier {
+ background: var(--codeBlockBgColor);
+ padding: 0 0.4em;
+ border-radius: 3px;
+ font-size: .7em;
+ color: var(--editorColor80);
+}
+
+.ag-inline-footnote-identifier a {
+ color: var(--editorColor);
+}
+
+i.ag-footnote-backlink {
+ width: 20px;
+ height: 20px;
+ text-align: center;
+ line-height: 20px;
+ display: block;
+ position: absolute;
+ right: .5em;
+ bottom: .5em;
+ font-family: sans-serif;
+ cursor: pointer;
+ z-index: 100;
+}
diff --git a/src/muya/lib/config/index.js b/src/muya/lib/config/index.js
index 3b2c1a97..fe291b6a 100644
--- a/src/muya/lib/config/index.js
+++ b/src/muya/lib/config/index.js
@@ -107,6 +107,7 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_INLINE_IMAGE_SELECTED',
'AG_INLINE_IMAGE_IS_EDIT',
'AG_INDENT_CODE',
+ 'AG_INLINE_FOOTNOTE_IDENTIFIER',
'AG_INLINE_RULE',
'AG_LANGUAGE',
'AG_LANGUAGE_INPUT',
@@ -276,7 +277,8 @@ export const MUYA_DEFAULT_OPTION = {
imagePathAutoComplete: () => [],
// Markdown extensions
- superSubScript: false
+ superSubScript: false,
+ footnote: false
}
// export const DIAGRAM_TEMPLATE = {
diff --git a/src/muya/lib/contentState/backspaceCtrl.js b/src/muya/lib/contentState/backspaceCtrl.js
index 3840f135..1d2dba1d 100644
--- a/src/muya/lib/contentState/backspaceCtrl.js
+++ b/src/muya/lib/contentState/backspaceCtrl.js
@@ -345,6 +345,29 @@ const backspaceCtrl = ContentState => {
}
if (
+ block.type === 'span' &&
+ block.functionType === 'paragraphContent' &&
+ left === 0 &&
+ preBlock &&
+ preBlock.functionType === 'footnoteInput'
+ ) {
+ event.preventDefault()
+ event.stopPropagation()
+ if (!parent.nextSibling) {
+ const pBlock = this.createBlockP(block.text)
+ const figureBlock = this.closest(block, 'figure')
+ this.insertBefore(pBlock, figureBlock)
+ this.removeBlock(figureBlock)
+ const key = pBlock.children[0].key
+ const offset = 0
+ this.cursor = {
+ start: { key, offset },
+ end: { key, offset }
+ }
+
+ this.partialRender()
+ }
+ } else if (
block.type === 'span' &&
block.functionType === 'codeContent' &&
left === 0 &&
@@ -492,7 +515,7 @@ const backspaceCtrl = ContentState => {
// also need to remove the paragrah
if (this.isOnlyChild(block) && block.type === 'span') {
this.removeBlock(parent)
- } else if (block.functionType !== 'languageInput') {
+ } else if (block.functionType !== 'languageInput' && block.functionType !== 'footnoteInput') {
this.removeBlock(block)
}
@@ -500,10 +523,14 @@ const backspaceCtrl = ContentState => {
start: { key, offset },
end: { key, offset }
}
- if (this.isCollapse()) {
+ let needRenderAll = false
+
+ if (this.isCollapse() && preBlock.type === 'span' && preBlock.functionType === 'paragraphContent') {
this.checkInlineUpdate(preBlock)
+ needRenderAll = true
}
- this.partialRender()
+
+ needRenderAll ? this.render() : this.partialRender()
}
}
}
diff --git a/src/muya/lib/contentState/copyCutCtrl.js b/src/muya/lib/contentState/copyCutCtrl.js
index 76106150..2395f114 100644
--- a/src/muya/lib/contentState/copyCutCtrl.js
+++ b/src/muya/lib/contentState/copyCutCtrl.js
@@ -191,7 +191,7 @@ const copyCutCtrl = ContentState => {
}
let htmlData = wrapper.innerHTML
- const textData = this.htmlToMarkdown(htmlData)
+ const textData = escapeHtml(this.htmlToMarkdown(htmlData))
htmlData = marked(textData)
return { html: htmlData, text: textData }
diff --git a/src/muya/lib/contentState/enterCtrl.js b/src/muya/lib/contentState/enterCtrl.js
index db084aa1..b326f320 100644
--- a/src/muya/lib/contentState/enterCtrl.js
+++ b/src/muya/lib/contentState/enterCtrl.js
@@ -1,6 +1,10 @@
import selection from '../selection'
import { isOsx } from '../config'
+/* eslint-disable no-useless-escape */
+const FOOTNOTE_REG = /^\[\^([^\^\[\]\s]+?)(? {
const pairStr = text.substring(offset - 1, offset + 1)
return /^(\{\}|\[\]|\(\)|><)$/.test(pairStr)
@@ -226,6 +230,26 @@ const enterCtrl = ContentState => {
return this.enterHandler(event)
}
+ if (
+ block.type === 'span' &&
+ block.functionType === 'paragraphContent' &&
+ !this.getParent(block).parent &&
+ start.offset === text.length &&
+ FOOTNOTE_REG.test(text)
+ ) {
+ event.preventDefault()
+ event.stopPropagation()
+ // Just to feet the `updateFootnote` API and add one white space.
+ block.text += ' '
+ const key = block.key
+ const offset = block.text.length
+ this.cursor = {
+ start: { key, offset },
+ end: { key, offset }
+ }
+ return this.updateFootnote(this.getParent(block), block)
+ }
+
// handle `shift + enter` insert `soft line break` or `hard line break`
// only cursor in `line block` can create `soft line break` and `hard line break`
// handle line in code block
@@ -418,6 +442,7 @@ const enterCtrl = ContentState => {
}
this.insertAfter(newBlock, block)
+
break
}
case left === 0 && right === 0: {
@@ -511,7 +536,14 @@ const enterCtrl = ContentState => {
end: { key, offset }
}
- this.partialRender()
+ let needRenderAll = false
+
+ if (this.isCollapse() && cursorBlock.type === 'p') {
+ this.checkInlineUpdate(cursorBlock.children[0])
+ needRenderAll = true
+ }
+
+ needRenderAll ? this.render() : this.partialRender()
}
}
diff --git a/src/muya/lib/contentState/footnoteCtrl.js b/src/muya/lib/contentState/footnoteCtrl.js
new file mode 100644
index 00000000..da9195ae
--- /dev/null
+++ b/src/muya/lib/contentState/footnoteCtrl.js
@@ -0,0 +1,63 @@
+/* eslint-disable no-useless-escape */
+const FOOTNOTE_REG = /^\[\^([^\^\[\]\s]+?)(? {
+ 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
diff --git a/src/muya/lib/contentState/formatCtrl.js b/src/muya/lib/contentState/formatCtrl.js
index 5027138d..1ab8e536 100644
--- a/src/muya/lib/contentState/formatCtrl.js
+++ b/src/muya/lib/contentState/formatCtrl.js
@@ -86,7 +86,12 @@ const clearFormat = (token, { start, end }) => {
}
const addFormat = (type, block, { start, end }) => {
- if (block.type === 'pre') return false
+ if (
+ block.type !== 'span' ||
+ (block.type === 'span' && !/paragraphContent|cellConntent|atxLine/.test(block.functionType))
+ ) {
+ return false
+ }
switch (type) {
case 'em':
case 'del':
diff --git a/src/muya/lib/contentState/imageCtrl.js b/src/muya/lib/contentState/imageCtrl.js
index 7279bec0..7779f328 100644
--- a/src/muya/lib/contentState/imageCtrl.js
+++ b/src/muya/lib/contentState/imageCtrl.js
@@ -29,7 +29,7 @@ const imageCtrl = ContentState => {
// Only encode URLs but not local paths or data URLs
let imgUrl
if (!/data:image/.test(src)) {
- imgUrl = encodeURI(src)
+ imgUrl = encodeURI(src).replace(/#/g, encodeURIComponent('#'))
} else {
imgUrl = src
}
@@ -132,7 +132,7 @@ const imageCtrl = ContentState => {
}
imageText += ']('
if (src) {
- imageText += encodeURI(src)
+ imageText += encodeURI(src).replace(/#/g, encodeURIComponent('#'))
}
if (title) {
imageText += ` "${title}"`
@@ -177,11 +177,19 @@ const imageCtrl = ContentState => {
this.selectedImage = imageInfo
const { key } = imageInfo
const block = this.getBlock(key)
+ const outMostBlock = this.findOutMostBlock(block)
this.cursor = {
start: { key, offset: imageInfo.token.range.end },
end: { key, offset: imageInfo.token.range.end }
}
- return this.singleRender(block, true)
+ // Fix #1568
+ const { start } = this.prevCursor
+ const oldBlock = this.findOutMostBlock(this.getBlock(start.key))
+ if (oldBlock.key !== outMostBlock.key) {
+ this.singleRender(oldBlock, false)
+ }
+
+ return this.singleRender(outMostBlock, true)
}
}
diff --git a/src/muya/lib/contentState/index.js b/src/muya/lib/contentState/index.js
index 47ce4214..735dd93b 100644
--- a/src/muya/lib/contentState/index.js
+++ b/src/muya/lib/contentState/index.js
@@ -28,6 +28,7 @@ import emojiCtrl from './emojiCtrl'
import imageCtrl from './imageCtrl'
import linkCtrl from './linkCtrl'
import dragDropCtrl from './dragDropCtrl'
+import footnoteCtrl from './footnoteCtrl'
import importMarkdown from '../utils/importMarkdown'
import Cursor from '../selection/cursor'
import escapeCharactersMap, { escapeCharacters } from '../parser/escapeCharacter'
@@ -58,6 +59,7 @@ const prototypes = [
imageCtrl,
linkCtrl,
dragDropCtrl,
+ footnoteCtrl,
importMarkdown
]
diff --git a/src/muya/lib/contentState/updateCtrl.js b/src/muya/lib/contentState/updateCtrl.js
index fadccfab..baedb065 100644
--- a/src/muya/lib/contentState/updateCtrl.js
+++ b/src/muya/lib/contentState/updateCtrl.js
@@ -10,6 +10,7 @@ const INLINE_UPDATE_FRAGMENTS = [
'^(?:[\\s\\S]+?)\\n {0,3}(\\={3,}|\\-{3,})(?= {1,}|$)', // Setext headings **match from beginning**
'(?:^|\n) {0,3}(>).+', // Block quote
'^( {4,})', // Indent code **match from beginning**
+ '^(\\[\\^[^\\^\\[\\]\\s]+?(? {
if (/figure/.test(block.type)) {
return false
}
- if (/cellContent|codeContent|languageInput/.test(block.functionType)) {
+ if (/cellContent|codeContent|languageInput|footnoteInput/.test(block.functionType)) {
return false
}
@@ -89,8 +90,9 @@ const updateCtrl = ContentState => {
const listItem = this.getParent(block)
const [
match, bullet, tasklist, order, atxHeader,
- setextHeader, blockquote, indentCode, hr
+ setextHeader, blockquote, indentCode, footnote, hr
] = text.match(INLINE_UPDATE_REG) || []
+ const { footnote: isSupportFootnote } = this.muya.options
switch (true) {
case (!!hr && new Set(hr.split('').filter(i => /\S/.test(i))).size === 1):
@@ -118,6 +120,9 @@ const updateCtrl = ContentState => {
case !!indentCode:
return this.updateIndentCode(block, line)
+ case !!footnote && block.type === 'p' && !block.parent && isSupportFootnote:
+ return this.updateFootnote(block, line)
+
case !match:
default:
return this.updateToParagraph(block, line)
diff --git a/src/muya/lib/eventHandler/clickEvent.js b/src/muya/lib/eventHandler/clickEvent.js
index 1014b1f8..157f2a49 100644
--- a/src/muya/lib/eventHandler/clickEvent.js
+++ b/src/muya/lib/eventHandler/clickEvent.js
@@ -101,6 +101,7 @@ class ClickEvent {
const rubyRender = target.closest(`.${CLASS_OR_ID.AG_RUBY_RENDER}`)
const imageWrapper = target.closest(`.${CLASS_OR_ID.AG_INLINE_IMAGE}`)
const codeCopy = target.closest('.ag-code-copy')
+ const footnoteBackLink = target.closest('.ag-footnote-backlink')
const imageDelete = target.closest('.ag-image-icon-delete') || target.closest('.ag-image-icon-close')
const mathText = mathRender && mathRender.previousElementSibling
const rubyText = rubyRender && rubyRender.previousElementSibling
@@ -131,6 +132,20 @@ class ClickEvent {
return contentState.deleteImage(imageInfo)
}
+ if (footnoteBackLink) {
+ event.preventDefault()
+ event.stopPropagation()
+ const figure = event.target.closest('figure')
+ const identifier = figure.querySelector('span.ag-footnote-input').textContent
+ if (identifier) {
+ const footnoteIdentifier = document.querySelector(`#noteref-${identifier}`)
+ if (footnoteIdentifier) {
+ footnoteIdentifier.scrollIntoView({ behavior: 'smooth' })
+ }
+ }
+ return
+ }
+
// Handle image click, to select the current image
if (target.tagName === 'IMG' && imageWrapper) {
// Handle select image
diff --git a/src/muya/lib/eventHandler/mouseEvent.js b/src/muya/lib/eventHandler/mouseEvent.js
index 3aed62b7..ca7fff02 100644
--- a/src/muya/lib/eventHandler/mouseEvent.js
+++ b/src/muya/lib/eventHandler/mouseEvent.js
@@ -1,4 +1,5 @@
import { getLinkInfo } from '../utils/getLinkInfo'
+import { collectFootnotes } from '../utils'
class MouseEvent {
constructor (muya) {
@@ -12,30 +13,69 @@ class MouseEvent {
const handler = event => {
const target = event.target
const parent = target.parentNode
- const { hideLinkPopup } = this.muya.options
- if (!hideLinkPopup && parent && parent.tagName === 'A' && parent.classList.contains('ag-inline-rule')) {
- const rect = parent.getBoundingClientRect()
- const reference = {
- getBoundingClientRect () {
- return rect
- }
+ const preSibling = target.previousElementSibling
+ const parentPreSibling = parent ? parent.previousElementSibling : null
+ const { hideLinkPopup, footnote } = this.muya.options
+ const rect = parent.getBoundingClientRect()
+ const reference = {
+ getBoundingClientRect () {
+ return rect
}
+ }
+ if (
+ !hideLinkPopup &&
+ parent &&
+ parent.tagName === 'A' &&
+ parent.classList.contains('ag-inline-rule') &&
+ parentPreSibling &&
+ parentPreSibling.classList.contains('ag-hide')
+ ) {
eventCenter.dispatch('muya-link-tools', {
reference,
linkInfo: getLinkInfo(parent)
})
}
+
+ if (
+ footnote &&
+ parent &&
+ parent.tagName === 'SUP' &&
+ parent.classList.contains('ag-inline-footnote-identifier') &&
+ preSibling &&
+ preSibling.classList.contains('ag-hide')
+ ) {
+ const identifier = target.textContent
+ eventCenter.dispatch('muya-footnote-tool', {
+ reference,
+ identifier,
+ footnotes: collectFootnotes(this.muya.contentState.blocks)
+ })
+ }
}
const leaveHandler = event => {
const target = event.target
const parent = target.parentNode
-
+ const preSibling = target.previousElementSibling
+ const { footnote } = this.muya.options
if (parent && parent.tagName === 'A' && parent.classList.contains('ag-inline-rule')) {
eventCenter.dispatch('muya-link-tools', {
reference: null
})
}
+
+ if (
+ footnote &&
+ parent &&
+ parent.tagName === 'SUP' &&
+ parent.classList.contains('ag-inline-footnote-identifier') &&
+ preSibling &&
+ preSibling.classList.contains('ag-hide')
+ ) {
+ eventCenter.dispatch('muya-footnote-tool', {
+ reference: null
+ })
+ }
}
eventCenter.attachDOMEvent(container, 'mouseover', handler)
diff --git a/src/muya/lib/parser/index.js b/src/muya/lib/parser/index.js
index ec7dc9a9..d1e48033 100644
--- a/src/muya/lib/parser/index.js
+++ b/src/muya/lib/parser/index.js
@@ -32,12 +32,12 @@ const correctUrl = token => {
}
}
-const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
+const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels, options) => {
const originSrc = src
const tokens = []
let pending = ''
let pendingStartPos = pos
-
+ const { superSubScript, footnote } = options
const pushPending = () => {
if (pending) {
tokens.push({
@@ -151,7 +151,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
range,
marker,
parent: tokens,
- children: tokenizerFac(to[2], undefined, inlineRules, pos + to[1].length, false, labels),
+ children: tokenizerFac(to[2], undefined, inlineRules, pos + to[1].length, false, labels, options),
backlash: to[3]
})
src = src.substring(to[0].length)
@@ -192,7 +192,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
range,
marker,
parent: tokens,
- children: tokenizerFac(to[2], undefined, inlineRules, pos + to[1].length, false, labels),
+ children: tokenizerFac(to[2], undefined, inlineRules, pos + to[1].length, false, labels, options),
backlash: to[3]
})
}
@@ -203,7 +203,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
}
if (inChunk) continue
// superscript and subscript
- if (inlineRules.superscript && inlineRules.subscript) {
+ if (superSubScript) {
const superSubTo = inlineRules.superscript.exec(src) || inlineRules.subscript.exec(src)
if (superSubTo) {
pushPending()
@@ -223,6 +223,28 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
continue
}
}
+
+ // footnote identifier
+ if (pos !== 0 && footnote) {
+ const footnoteTo = inlineRules.footnote_identifier.exec(src)
+ if (footnoteTo) {
+ pushPending()
+ tokens.push({
+ type: 'footnote_identifier',
+ raw: footnoteTo[0],
+ marker: footnoteTo[1],
+ range: {
+ start: pos,
+ end: pos + footnoteTo[0].length
+ },
+ parent: tokens,
+ content: footnoteTo[2]
+ })
+ src = src.substring(footnoteTo[0].length)
+ pos = pos + footnoteTo[0].length
+ continue
+ }
+ }
// image
const imageTo = inlineRules.image.exec(src)
correctUrl(imageTo)
@@ -276,7 +298,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
start: pos,
end: pos + linkTo[0].length
},
- children: tokenizerFac(linkTo[2], undefined, inlineRules, pos + linkTo[1].length, false, labels),
+ children: tokenizerFac(linkTo[2], undefined, inlineRules, pos + linkTo[1].length, false, labels, options),
backlash: {
first: linkTo[3],
second: linkTo[5]
@@ -306,7 +328,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
start: pos,
end: pos + rLinkTo[0].length
},
- children: tokenizerFac(rLinkTo[1], undefined, inlineRules, pos + 1, false, labels)
+ children: tokenizerFac(rLinkTo[1], undefined, inlineRules, pos + 1, false, labels, options)
})
src = src.substring(rLinkTo[0].length)
@@ -442,7 +464,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
parent: tokens,
attrs,
content: htmlTo[4],
- children: htmlTo[4] ? tokenizerFac(htmlTo[4], undefined, inlineRules, pos + htmlTo[2].length, false, labels) : '',
+ children: htmlTo[4] ? tokenizerFac(htmlTo[4], undefined, inlineRules, pos + htmlTo[2].length, false, labels, options) : '',
range: {
start: pos,
end: pos + len
@@ -530,16 +552,8 @@ export const tokenizer = (src, {
labels = new Map(),
options = {}
} = {}) => {
- const { superSubScript } = options
-
- if (superSubScript) {
- inlineRules.superscript = inlineExtensionRules.superscript
- inlineRules.subscript = inlineExtensionRules.subscript
- } else {
- delete inlineRules.superscript
- delete inlineRules.subscript
- }
- const tokens = tokenizerFac(src, hasBeginRules ? beginRules : null, inlineRules, 0, true, labels)
+ const rules = Object.assign({}, inlineRules, inlineExtensionRules)
+ const tokens = tokenizerFac(src, hasBeginRules ? beginRules : null, rules, 0, true, labels, options)
const postTokenizer = tokens => {
for (const token of tokens) {
diff --git a/src/muya/lib/parser/marked/blockRules.js b/src/muya/lib/parser/marked/blockRules.js
index 1b5d1033..99774b2b 100644
--- a/src/muya/lib/parser/marked/blockRules.js
+++ b/src/muya/lib/parser/marked/blockRules.js
@@ -35,7 +35,8 @@ export const block = {
// extra
frontmatter: /^(?:(?:---\n([\s\S]+?)---)|(?:\+\+\+\n([\s\S]+?)\+\+\+)|(?:;;;\n([\s\S]+?);;;)|(?:\{\n([\s\S]+?)\}))(?:\n{2,}|\n{1,2}$)/,
- multiplemath: /^\$\$\n([\s\S]+?)\n\$\$(?:\n+|$)/
+ multiplemath: /^\$\$\n([\s\S]+?)\n\$\$(?:\n+|$)/,
+ footnote: /^\[\^([^\^\[\]\s]+?)\]:[\s\S]+?(?=\n *\n {0,3}[^ ]+|$)/
}
block._label = /(?!\s*\])(?:\\[\[\]]|[^\[\]])+/
diff --git a/src/muya/lib/parser/marked/inlineLexer.js b/src/muya/lib/parser/marked/inlineLexer.js
index 359461f3..6349c8c7 100644
--- a/src/muya/lib/parser/marked/inlineLexer.js
+++ b/src/muya/lib/parser/marked/inlineLexer.js
@@ -1,16 +1,17 @@
import Renderer from './renderer'
import { normal, breaks, gfm, pedantic } from './inlineRules'
import defaultOptions from './options'
-import { escape, findClosingBracket } from './utils'
+import { escape, findClosingBracket, getUniqueId } from './utils'
import { validateEmphasize, lowerPriority } from '../utils'
/**
* Inline Lexer & Compiler
*/
-function InlineLexer (links, options) {
+function InlineLexer (links, footnotes, options) {
this.options = options || defaultOptions
this.links = links
+ this.footnotes = footnotes
this.rules = normal
this.renderer = this.options.renderer || new Renderer()
this.renderer.options = this.options
@@ -49,7 +50,7 @@ function InlineLexer (links, options) {
InlineLexer.prototype.output = function (src) {
// src = src
// .replace(/\u00a0/g, ' ')
- const { disableInline, emoji, math, superSubScript } = this.options
+ const { disableInline, emoji, math, superSubScript, footnote } = this.options
if (disableInline) {
return escape(src)
}
@@ -73,6 +74,19 @@ InlineLexer.prototype.output = function (src) {
continue
}
+ // footnote identifier
+ if (footnote) {
+ cap = this.rules.footnoteIdentifier.exec(src)
+ if (cap) {
+ src = src.substring(cap[0].length)
+ lastChar = cap[0].charAt(cap[0].length - 1)
+ const identifier = cap[1]
+ const footnoteInfo = this.footnotes[identifier] || {}
+ footnoteInfo.footnoteIdentifierId = getUniqueId()
+ out += this.renderer.footnoteIdentifier(identifier, footnoteInfo)
+ }
+ }
+
// tag
cap = this.rules.tag.exec(src)
if (cap) {
diff --git a/src/muya/lib/parser/marked/inlineRules.js b/src/muya/lib/parser/marked/inlineRules.js
index 8d0f6330..5eb11530 100644
--- a/src/muya/lib/parser/marked/inlineRules.js
+++ b/src/muya/lib/parser/marked/inlineRules.js
@@ -29,7 +29,7 @@ const inline = {
// ------------------------
// patched
- // allow inline math "$" and superscript ("?=[\\ 1
@@ -367,7 +425,7 @@ Lexer.prototype.token = function (src, top) {
const isOrderedListItem = /\d/.test(bull)
this.tokens.push({
- checked: checked,
+ checked,
listItemType: bull.length > 1 ? 'order' : (isTaskList ? 'task' : 'bullet'),
bulletMarkerOrDelimiter: isOrderedListItem ? bull.slice(-1) : bull.charAt(0),
type: loose ? 'loose_item_start' : 'list_item_start'
@@ -534,8 +592,6 @@ Lexer.prototype.token = function (src, top) {
throw new Error('Infinite loop on byte: ' + src.charCodeAt(0))
}
}
-
- return this.tokens
}
export default Lexer
diff --git a/src/muya/lib/parser/marked/options.js b/src/muya/lib/parser/marked/options.js
index e4631026..3d3a8158 100644
--- a/src/muya/lib/parser/marked/options.js
+++ b/src/muya/lib/parser/marked/options.js
@@ -28,5 +28,6 @@ export default {
emoji: true,
math: true,
frontMatter: true,
- superSubScript: false
+ superSubScript: false,
+ footnote: false
}
diff --git a/src/muya/lib/parser/marked/parser.js b/src/muya/lib/parser/marked/parser.js
index 33dcbeb8..f4084fef 100644
--- a/src/muya/lib/parser/marked/parser.js
+++ b/src/muya/lib/parser/marked/parser.js
@@ -11,6 +11,8 @@ import defaultOptions from './options'
function Parser (options) {
this.tokens = []
this.token = null
+ this.footnotes = null
+ this.footnoteIdentifier = ''
this.options = options || defaultOptions
this.options.renderer = this.options.renderer || new Renderer()
this.renderer = this.options.renderer
@@ -23,14 +25,15 @@ function Parser (options) {
*/
Parser.prototype.parse = function (src) {
- this.inline = new InlineLexer(src.links, this.options)
+ this.inline = new InlineLexer(src.links, src.footnotes, this.options)
// use an InlineLexer with a TextRenderer to extract pure text
this.inlineText = new InlineLexer(
src.links,
+ src.footnotes,
Object.assign({}, this.options, { renderer: new TextRenderer() })
)
this.tokens = src.reverse()
-
+ this.footnotes = src.footnotes
let out = ''
while (this.next()) {
out += this.tok()
@@ -148,6 +151,27 @@ Parser.prototype.tok = function () {
return this.renderer.blockquote(body)
}
+ // All the tokens will be footnotes if it after a footnote_start token. Because we put all footnote token at the end.
+ case 'footnote_start': {
+ let body = ''
+ let itemBody = ''
+ this.footnoteIdentifier = this.token.identifier
+ while (this.next()) {
+ if (this.token.type === 'footnote_end') {
+ const footnoteInfo = this.footnotes[this.footnoteIdentifier]
+ body += this.renderer.footnoteItem(itemBody, footnoteInfo)
+ this.footnoteIdentifier = ''
+ itemBody = ''
+ } else if (this.token.type === 'footnote_start') {
+ this.footnoteIdentifier = this.token.identifier
+ itemBody = ''
+ } else {
+ itemBody += this.tok()
+ }
+ }
+
+ return this.renderer.footnote(body)
+ }
case 'list_start': {
let body = ''
let taskList = false
diff --git a/src/muya/lib/parser/marked/renderer.js b/src/muya/lib/parser/marked/renderer.js
index 451e30db..b25730dc 100644
--- a/src/muya/lib/parser/marked/renderer.js
+++ b/src/muya/lib/parser/marked/renderer.js
@@ -44,6 +44,18 @@ Renderer.prototype.script = function (content, marker) {
return `<${tagName}>${content}${tagName}>`
}
+Renderer.prototype.footnoteIdentifier = function (identifier, { footnoteId, footnoteIdentifierId, order }) {
+ return `${order || identifier}`
+}
+
+Renderer.prototype.footnote = function (footnote) {
+ return '
\n\n' + footnote + '
\n