From 5d13acb1e05df0b45820a002877b27aeabbdf2eb Mon Sep 17 00:00:00 2001 From: yanglbme Date: Sat, 7 Dec 2019 18:03:26 +0800 Subject: [PATCH] feat: close brackets automatically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 自动关闭括号 --- assets/scripts/editor.js | 1 + assets/scripts/renderers/wx-renderer.js | 1 - index.html | 2 +- libs/scripts/FuriganaMD.js | 241 ------------------------ libs/scripts/closebrackets.js | 191 +++++++++++++++++++ 5 files changed, 193 insertions(+), 243 deletions(-) delete mode 100644 libs/scripts/FuriganaMD.js create mode 100644 libs/scripts/closebrackets.js diff --git a/assets/scripts/editor.js b/assets/scripts/editor.js index d964984..d448657 100644 --- a/assets/scripts/editor.js +++ b/assets/scripts/editor.js @@ -46,6 +46,7 @@ let app = new Vue({ lineNumbers: false, lineWrapping: true, styleActiveLine: true, + autoCloseBrackets: true, theme: this.currentEditorTheme, mode: 'text/x-markdown' } diff --git a/assets/scripts/renderers/wx-renderer.js b/assets/scripts/renderers/wx-renderer.js index b2fa4d0..8a441ff 100644 --- a/assets/scripts/renderers/wx-renderer.js +++ b/assets/scripts/renderers/wx-renderer.js @@ -98,7 +98,6 @@ let WxRenderer = function (opts) { styleMapping = this.buildTheme(this.opts.theme); let renderer = new marked.Renderer(); - FuriganaMD.register(renderer); renderer.heading = (text, level) => { switch (level) { diff --git a/index.html b/index.html index 3ecdaac..26730c3 100644 --- a/index.html +++ b/index.html @@ -128,7 +128,7 @@ - + diff --git a/libs/scripts/FuriganaMD.js b/libs/scripts/FuriganaMD.js deleted file mode 100644 index 93abc5a..0000000 --- a/libs/scripts/FuriganaMD.js +++ /dev/null @@ -1,241 +0,0 @@ -/** - * 注音功能来自于:https://github.com/amclees/furigana-markdown - * 详见上述文档 - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global.FuriganaMD = factory()); -}(this, (function () { - 'use strict'; - - // This function escapes special characters for use in a regex constructor. - function escapeForRegex(string) { - return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - } - - function emptyStringFilter(block) { - return block !== ''; - } - - const kanjiRange = '\\u4e00-\\u9faf'; - const kanjiBlockRegex = new RegExp(`[${kanjiRange}]+`, 'g'); - const nonKanjiBlockRegex = new RegExp(`[^${kanjiRange}]+`, 'g'); - const kanaWithAnnotations = '\\u3041-\\u3095\\u3099-\\u309c\\u3081-\\u30fa\\u30fc'; - const furiganaSeperators = '..。・'; - const seperatorRegex = new RegExp(`[${furiganaSeperators}]`, 'g'); - - const singleKanjiRegex = new RegExp(`^[${kanjiRange}]$`); - - function isKanji(character) { - return character.match(singleKanjiRegex); - } - - const innerRegexString = '(?:[^\\u0000-\\u007F]|\\w)+'; - - let regexList = []; - let previousFuriganaForms = ''; - - function updateRegexList(furiganaForms) { - previousFuriganaForms = furiganaForms; - let formArray = furiganaForms.split('|'); - if (formArray.length === 0) { - formArray = ['[]:^:()']; - } - regexList = formArray.map(form => { - let furiganaComponents = form.split(':'); - if (furiganaComponents.length !== 3) { - furiganaComponents = ['[]', '^', '()']; - } - const mainBrackets = furiganaComponents[0]; - const seperator = furiganaComponents[1]; - const furiganaBrackets = furiganaComponents[2]; - return new RegExp( - escapeForRegex(mainBrackets[0]) + - '(' + innerRegexString + ')' + - escapeForRegex(mainBrackets[1]) + - escapeForRegex(seperator) + - escapeForRegex(furiganaBrackets[0]) + - '(' + innerRegexString + ')' + - escapeForRegex(furiganaBrackets[1]), - 'g' - ); - }); - } - - let autoRegexList = []; - let previousAutoBracketSets = ''; - - function updateAutoRegexList(autoBracketSets) { - previousAutoBracketSets = autoBracketSets; - autoRegexList = autoBracketSets.split('|').map(brackets => { - /* - Sample built regex: - /(^|[^\u4e00-\u9faf]|)([\u4e00-\u9faf]+)([\u3041-\u3095\u3099-\u309c\u3081-\u30fa\u30fc]*)【((?:[^【】\u4e00-\u9faf]|w)+)】/g - */ - return new RegExp( - `(^|[^${kanjiRange}]|)` + - `([${kanjiRange}]+)` + - `([${kanaWithAnnotations}]*)` + - escapeForRegex(brackets[0]) + - `((?:[^${escapeForRegex(brackets)}\\u0000-\\u007F]|\\w|[${furiganaSeperators}])+)` + - escapeForRegex(brackets[1]), - 'g' - ); - }); - } - - let replacementTemplate = ''; - let replacementBrackets = ''; - - function updateReplacementTemplate(furiganaFallbackBrackets) { - if (furiganaFallbackBrackets.length !== 2) { - furiganaFallbackBrackets = '【】'; - } - replacementBrackets = furiganaFallbackBrackets; - replacementTemplate = `$1${furiganaFallbackBrackets[0]}$2${furiganaFallbackBrackets[1]}`; - } - - updateReplacementTemplate('【】'); - - function addFurigana(text, options) { - if (options.furiganaForms !== previousFuriganaForms) { - updateRegexList(options.furiganaForms); - } - if (options.furiganaFallbackBrackets !== replacementBrackets) { - updateReplacementTemplate(options.furiganaFallbackBrackets); - } - regexList.forEach(regex => { - text = text.replace(regex, (match, wordText, furiganaText, offset, mainText) => { - if (match.indexOf('\\') === -1 && mainText[offset - 1] !== '\\') { - if ((!options.furiganaPatternMatching) || wordText.search(kanjiBlockRegex) === -1 || wordText[0].search(kanjiBlockRegex) === -1) { - return replacementTemplate.replace('$1', wordText).replace('$2', furiganaText); - } else { - let originalFuriganaText = (' ' + furiganaText).slice(1); - let nonKanji = wordText.split(kanjiBlockRegex).filter(emptyStringFilter); - let kanji = wordText.split(nonKanjiBlockRegex).filter(emptyStringFilter); - let replacementText = ''; - let lastUsedKanjiIndex = 0; - if (nonKanji.length === 0) { - return replacementTemplate.replace('$1', wordText).replace('$2', furiganaText); - } - - nonKanji.forEach((currentNonKanji, index) => { - if (furiganaText === undefined) { - if (index < kanji.length) { - replacementText += kanji[index]; - } - - replacementText += currentNonKanji; - return; - } - let splitFurigana = furiganaText.split(new RegExp(escapeForRegex(currentNonKanji) + '(.*)')).filter(emptyStringFilter); - - lastUsedKanjiIndex = index; - replacementText += replacementTemplate.replace('$1', kanji[index]).replace('$2', splitFurigana[0]); - replacementText += currentNonKanji; - - furiganaText = splitFurigana[1]; - }); - if (furiganaText !== undefined && lastUsedKanjiIndex + 1 < kanji.length) { - replacementText += replacementTemplate.replace('$1', kanji[lastUsedKanjiIndex + 1]).replace('$2', furiganaText); - } else if (furiganaText !== undefined) { - return replacementTemplate.replace('$1', wordText).replace('$2', originalFuriganaText); - } else if (lastUsedKanjiIndex + 1 < kanji.length) { - replacementText += kanji[lastUsedKanjiIndex + 1]; - } - return replacementText; - } - } else { - return match; - } - }); - }); - - if (!options.furiganaStrictMode) { - if (options.furiganaAutoBracketSets !== previousAutoBracketSets) { - updateAutoRegexList(options.furiganaAutoBracketSets); - } - autoRegexList.forEach(regex => { - text = text.replace(regex, (match, preWordTerminator, wordKanji, wordKanaSuffix, furiganaText, offset, mainText) => { - if (match.indexOf('\\') === -1) { - if (options.furiganaPatternMatching) { - let rubies = []; - - let furigana = furiganaText; - - let stem = (' ' + wordKanaSuffix).slice(1); - for (let i = furiganaText.length - 1; i >= 0; i--) { - if (wordKanaSuffix.length === 0) { - furigana = furiganaText.substring(0, i + 1); - break; - } - if (furiganaText[i] !== wordKanaSuffix.slice(-1)) { - furigana = furiganaText.substring(0, i + 1); - break; - } - wordKanaSuffix = wordKanaSuffix.slice(0, -1); - } - - if (furiganaSeperators.split('').reduce( - (noSeperator, seperator) => { - return noSeperator && (furigana.indexOf(seperator) === -1); - }, - true - )) { - rubies = [replacementTemplate.replace('$1', wordKanji).replace('$2', furigana)]; - } else { - let kanaParts = furigana.split(seperatorRegex); - let kanji = wordKanji.split(''); - if (kanaParts.length === 0 || kanaParts.length > kanji.length) { - rubies = [replacementTemplate.replace('$1', wordKanji).replace('$2', furigana)]; - } else { - for (let i = 0; i < kanaParts.length - 1; i++) { - if (kanji.length === 0) { - break; - } - rubies.push(replacementTemplate.replace('$1', kanji.shift()).replace('$2', kanaParts[i])); - } - let lastKanaPart = kanaParts.pop(); - rubies.push(replacementTemplate.replace('$1', kanji.join('')).replace('$2', lastKanaPart)); - } - } - - return preWordTerminator + rubies.join('') + stem; - } else { - return preWordTerminator + replacementTemplate.replace('$1', wordKanji).replace('$2', furiganaText) + wordKanaSuffix; - } - } else { - return match; - } - }); - }); - } - return text; - } - - function handleEscapedSpecialBrackets(text) { - // By default 【 and 】 cannot be escaped in markdown, this will remove backslashes from in front of them to give that effect. - return text.replace(/\\([【】])/g, '$1'); - } - - let FuriganaMD = {}; - FuriganaMD.register = function (renderer) { - renderer.text = function (text) { - let options = { - furigana: true, - furiganaForms: "()::{}", - furiganaFallbackBrackets: "{}", - furiganaStrictMode: false, - furiganaAutoBracketSets: "{}", - furiganaPatternMatching: true, - }; - // console.log('override text render',text); - // console.log('after add',addFurigana(text, options)); - return handleEscapedSpecialBrackets(addFurigana(text, options)); - }; - }; - - return FuriganaMD; - -}))); diff --git a/libs/scripts/closebrackets.js b/libs/scripts/closebrackets.js new file mode 100644 index 0000000..4415c39 --- /dev/null +++ b/libs/scripts/closebrackets.js @@ -0,0 +1,191 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + var defaults = { + pairs: "()[]{}''\"\"", + closeBefore: ")]}'\":;>", + triples: "", + explode: "[]{}" + }; + + var Pos = CodeMirror.Pos; + + CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + cm.removeKeyMap(keyMap); + cm.state.closeBrackets = null; + } + if (val) { + ensureBound(getOption(val, "pairs")) + cm.state.closeBrackets = val; + cm.addKeyMap(keyMap); + } + }); + + function getOption(conf, name) { + if (name == "pairs" && typeof conf == "string") return conf; + if (typeof conf == "object" && conf[name] != null) return conf[name]; + return defaults[name]; + } + + var keyMap = {Backspace: handleBackspace, Enter: handleEnter}; + function ensureBound(chars) { + for (var i = 0; i < chars.length; i++) { + var ch = chars.charAt(i), key = "'" + ch + "'" + if (!keyMap[key]) keyMap[key] = handler(ch) + } + } + ensureBound(defaults.pairs + "`") + + function handler(ch) { + return function(cm) { return handleChar(cm, ch); }; + } + + function getConfig(cm) { + var deflt = cm.state.closeBrackets; + if (!deflt || deflt.override) return deflt; + var mode = cm.getModeAt(cm.getCursor()); + return mode.closeBrackets || deflt; + } + + function handleBackspace(cm) { + var conf = getConfig(cm); + if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; + + var pairs = getOption(conf, "pairs"); + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + if (!ranges[i].empty()) return CodeMirror.Pass; + var around = charsAround(cm, ranges[i].head); + if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass; + } + for (var i = ranges.length - 1; i >= 0; i--) { + var cur = ranges[i].head; + cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete"); + } + } + + function handleEnter(cm) { + var conf = getConfig(cm); + var explode = conf && getOption(conf, "explode"); + if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass; + + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + if (!ranges[i].empty()) return CodeMirror.Pass; + var around = charsAround(cm, ranges[i].head); + if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass; + } + cm.operation(function() { + var linesep = cm.lineSeparator() || "\n"; + cm.replaceSelection(linesep + linesep, null); + cm.execCommand("goCharLeft"); + ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + var line = ranges[i].head.line; + cm.indentLine(line, null, true); + cm.indentLine(line + 1, null, true); + } + }); + } + + function contractSelection(sel) { + var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0; + return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)), + head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))}; + } + + function handleChar(cm, ch) { + var conf = getConfig(cm); + if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; + + var pairs = getOption(conf, "pairs"); + var pos = pairs.indexOf(ch); + if (pos == -1) return CodeMirror.Pass; + + var closeBefore = getOption(conf,"closeBefore"); + + var triples = getOption(conf, "triples"); + + var identical = pairs.charAt(pos + 1) == ch; + var ranges = cm.listSelections(); + var opening = pos % 2 == 0; + + var type; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i], cur = range.head, curType; + var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1)); + if (opening && !range.empty()) { + curType = "surround"; + } else if ((identical || !opening) && next == ch) { + if (identical && stringStartsAfter(cm, cur)) + curType = "both"; + else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch) + curType = "skipThree"; + else + curType = "skip"; + } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 && + cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch) { + if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass; + curType = "addFour"; + } else if (identical) { + var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur) + if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both"; + else return CodeMirror.Pass; + } else if (opening && (next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)) { + curType = "both"; + } else { + return CodeMirror.Pass; + } + if (!type) type = curType; + else if (type != curType) return CodeMirror.Pass; + } + + var left = pos % 2 ? pairs.charAt(pos - 1) : ch; + var right = pos % 2 ? ch : pairs.charAt(pos + 1); + cm.operation(function() { + if (type == "skip") { + cm.execCommand("goCharRight"); + } else if (type == "skipThree") { + for (var i = 0; i < 3; i++) + cm.execCommand("goCharRight"); + } else if (type == "surround") { + var sels = cm.getSelections(); + for (var i = 0; i < sels.length; i++) + sels[i] = left + sels[i] + right; + cm.replaceSelections(sels, "around"); + sels = cm.listSelections().slice(); + for (var i = 0; i < sels.length; i++) + sels[i] = contractSelection(sels[i]); + cm.setSelections(sels); + } else if (type == "both") { + cm.replaceSelection(left + right, null); + cm.triggerElectric(left + right); + cm.execCommand("goCharLeft"); + } else if (type == "addFour") { + cm.replaceSelection(left + left + left + left, "before"); + cm.execCommand("goCharRight"); + } + }); + } + + function charsAround(cm, pos) { + var str = cm.getRange(Pos(pos.line, pos.ch - 1), + Pos(pos.line, pos.ch + 1)); + return str.length == 2 ? str : null; + } + + function stringStartsAfter(cm, pos) { + var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1)) + return /\bstring/.test(token.type) && token.start == pos.ch && + (pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos))) + } +});