From d59702bb133835ce51cbcd55f5cfea2cea779a19 Mon Sep 17 00:00:00 2001 From: Jocs Date: Thu, 1 Mar 2018 03:03:10 +0800 Subject: [PATCH] feat: search value, find next find prev --- CHANGELOG.md | 18 +- TODOLIST.md | 2 +- package-lock.json | 2 +- package.json | 2 +- src/editor/config.js | 4 +- src/editor/contentState/index.js | 15 +- src/editor/contentState/paragraphCtrl.js | 2 + src/editor/contentState/searchCtrl.js | 98 ++++++++ src/editor/index.css | 35 ++- src/editor/index.js | 25 ++ src/editor/parser/StateRender.js | 295 +++++++++++++++++------ src/editor/parser/parse.js | 34 ++- src/editor/utils/importMarkdown.js | 2 +- src/editor/utils/index.js | 19 ++ src/main/menus/edit.js | 26 ++ src/renderer/App.vue | 11 +- src/renderer/components/Editor.vue | 24 +- src/renderer/components/search.vue | 247 ++++++++++++++++++- src/renderer/components/titleBar.vue | 3 +- src/renderer/main.js | 3 +- src/renderer/store/editor.js | 23 +- 21 files changed, 790 insertions(+), 100 deletions(-) create mode 100644 src/editor/contentState/searchCtrl.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e225a41d..ffad139c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,20 @@ -### 0.3.1 +### 0.4.0 + +**Feature** + +- Search value in document, Use **FIND PREV** and **FIND NEXT** to selection previous one or next one. + + Add animation of highlight word. + + Auto focus the search input when open search panel. + + close the search panel will auto selection the last highlight word by ESC button. + +- Replace value + + Replace All + + Replace one and auto highlight the next word. **Bug fix** diff --git a/TODOLIST.md b/TODOLIST.md index 8ad8fea3..198b5538 100644 --- a/TODOLIST.md +++ b/TODOLIST.md @@ -1,6 +1,6 @@ #### TODO LIST -- [ ] Support Search and Replacement. +- [x] Support Search and Replacement. - [ ] add Dark, Light and GitHub theme. diff --git a/package-lock.json b/package-lock.json index 0c3257c4..e8d51cf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "marktext", - "version": "0.2.2", + "version": "0.3.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b92a30f6..391dcb58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "marktext", - "version": "0.3.1", + "version": "0.4.0", "author": "Jocs ", "description": "A markdown editor", "license": "MIT", diff --git a/src/editor/config.js b/src/editor/config.js index 7985b64f..84dbd55c 100644 --- a/src/editor/config.js +++ b/src/editor/config.js @@ -80,7 +80,9 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([ 'AG_TASK_LIST_ITEM', 'AG_TASK_LIST_ITEM_CHECKBOX', 'AG_CHECKBOX_CHECKED', - 'AG_TABLE_TOOL_BAR' + 'AG_TABLE_TOOL_BAR', + 'AG_SELECTION', + 'AG_HIGHLIGHT' ]) export const codeMirrorConfig = { diff --git a/src/editor/contentState/index.js b/src/editor/contentState/index.js index 394a4cba..439421e6 100644 --- a/src/editor/contentState/index.js +++ b/src/editor/contentState/index.js @@ -15,6 +15,7 @@ import copyCutCtrl from './copyCutCtrl' import paragraphCtrl from './paragraphCtrl' import tabCtrl from './tabCtrl' import formatCtrl from './formatCtrl' +import searchCtrl from './searchCtrl' import importMarkdown from '../utils/importMarkdown' const prototypes = [ @@ -31,6 +32,7 @@ const prototypes = [ tableBlockCtrl, paragraphCtrl, formatCtrl, + searchCtrl, importMarkdown ] @@ -61,6 +63,11 @@ class ContentState { init () { const lastBlock = this.getLastBlock() + this.searchMatches = { + value: '', + matches: [], + index: -1 + } this.cursor = { start: { key: lastBlock.key, @@ -84,10 +91,12 @@ class ContentState { } render (isRenderCursor = true) { - const { blocks, cursor } = this + const { blocks, cursor, searchMatches: { matches, index } } = this const activeBlocks = this.getActiveBlocks() - - this.stateRender.render(blocks, cursor, activeBlocks) + matches.forEach((m, i) => { + m.active = i === index + }) + this.stateRender.render(blocks, cursor, activeBlocks, matches) if (isRenderCursor) this.setCursor() this.pre2CodeMirror() console.log('render') diff --git a/src/editor/contentState/paragraphCtrl.js b/src/editor/contentState/paragraphCtrl.js index 5cc2705b..8519f71c 100644 --- a/src/editor/contentState/paragraphCtrl.js +++ b/src/editor/contentState/paragraphCtrl.js @@ -25,7 +25,9 @@ const paragraphCtrl = ContentState => { .filter(p => PARAGRAPH_TYPES.includes(p.type)) start.type = startBlock.type + start.block = startBlock end.type = endBlock.type + end.block = endBlock return { start, diff --git a/src/editor/contentState/searchCtrl.js b/src/editor/contentState/searchCtrl.js new file mode 100644 index 00000000..3e31da5e --- /dev/null +++ b/src/editor/contentState/searchCtrl.js @@ -0,0 +1,98 @@ +const defaultSearchOption = { + caseSensitive: false, + selectHighlight: false, + highlightIndex: -1 +} + +const searchCtrl = ContentState => { + ContentState.prototype.replaceOne = function (match, value) { + const { + start, + end, + key + } = match + const block = this.getBlock(key) + const { + text + } = block + + block.text = text.substring(0, start) + value + text.substring(end) + } + + ContentState.prototype.replace = function (replaceValue, opt = { isSingle: true }) { + const { isSingle, caseSensitive } = opt + const { matches, value, index } = this.searchMatches + if (matches.length) { + if (isSingle) { + this.replaceOne(matches[index], replaceValue) + } else { + // replace all + for (const match of matches) { + this.replaceOne(match, replaceValue) + } + } + const highlightIndex = index < matches.length - 1 ? index : index - 1 + this.search(value, { caseSensitive, highlightIndex: isSingle ? highlightIndex : -1 }) + } + } + + ContentState.prototype.search = function (value, opt = {}) { + value = value.trim() + let matches = [] + const { caseSensitive, selectHighlight, highlightIndex } = Object.assign(defaultSearchOption, opt) + const { blocks } = this + const search = blocks => { + for (const block of blocks) { + let { text, key } = block + if (!caseSensitive) { + text = text.toLowerCase() + value = value.toLowerCase() + } + if (text) { + let i = text.indexOf(value) + while (i > -1) { + matches.push({ + key, + start: i, + end: i + value.length + }) + i = text.indexOf(value, i + value.length) + } + } + if (block.children.length) { + search(block.children) + } + } + } + if (value) search(blocks) + let index = -1 + if (highlightIndex !== -1) { + index = highlightIndex + } else if (matches.length) { + index = 0 + } + + if (selectHighlight) { + const { matches, index } = this.searchMatches + const light = matches[index] + if (light) { + const key = light.key + this.cursor = { + start: { + key, + offset: light.start + }, + end: { + key, + offset: light.end + } + } + } + } + Object.assign(this.searchMatches, { value, matches, index }) + + return matches + } +} + +export default searchCtrl diff --git a/src/editor/index.css b/src/editor/index.css index bfb5d0c4..dad4791c 100644 --- a/src/editor/index.css +++ b/src/editor/index.css @@ -1,3 +1,15 @@ +@keyframes highlight { + from { + transform: scale(1); + } + 50% { + transform: scale(1.2); + } /* ignored */ + to { + transform: scale(1); + } +} + h1.ag-active::before, h2.ag-active::before, h3.ag-active::before, @@ -13,16 +25,24 @@ h6.ag-active::before { position: absolute; top: 0; left: -25px; - border: 1px solid #ddd; - border-radius: 5px; + border: 1px solid #C0C4CC; + border-radius: 3px; font-size: 12px; - color: #ddd; + color: #C0C4CC; transform: scale(.7); font-weight: 100; } -*::selection { - background: #efefef; +*::selection, .ag-selection { + background: #E4E7ED; +} + +.ag-highlight { + animation-name: highlight; + animation-duration: .25s; + display: inline-block; + background: rgb(249, 226, 153); + color: #303133; } figure { @@ -242,7 +262,7 @@ pre.ag-active .ag-language-input { caret-color: #303133; } .ag-gray { - color: lavender; + color: #C0C4CC; text-decoration: none; } @@ -262,7 +282,8 @@ pre.ag-active .ag-language-input { color: #000; text-decoration: none; } -.ag-hide { + +.ag-hide, .ag-hide .ag-highlight, .ag-hide .ag-selection { display: inline-block; width: 0; height: 0; diff --git a/src/editor/index.js b/src/editor/index.js index e1ec1130..b017c56d 100644 --- a/src/editor/index.js +++ b/src/editor/index.js @@ -412,6 +412,31 @@ class Aganippe { this.contentState.format(type) } + search (value, opt) { + const { selectHighlight } = opt + this.contentState.search(value, opt) + this.contentState.render(!!selectHighlight) + return this.contentState.searchMatches + } + + replace (value, opt) { + this.contentState.replace(value, opt) + this.contentState.render(false) + return this.contentState.searchMatches + } + + find (action/* pre or next */) { + let { matches, index } = this.contentState.searchMatches + const len = matches.length + if (!len) return + index = action === 'next' ? index + 1 : index - 1 + if (index < 0) index = len - 1 + if (index >= len) index = 0 + this.contentState.searchMatches.index = index + this.contentState.render(false) + return this.contentState.searchMatches + } + on (event, listener) { const { eventCenter } = this eventCenter.subscribe(event, listener) diff --git a/src/editor/parser/StateRender.js b/src/editor/parser/StateRender.js index 70451dcb..0753aa67 100644 --- a/src/editor/parser/StateRender.js +++ b/src/editor/parser/StateRender.js @@ -1,9 +1,12 @@ import { LOWERCASE_TAGS, CLASS_OR_ID } from '../config' -import { conflict, isLengthEven, isEven, getIdWithoutSet, loadImage, getImageSrc } from '../utils' +import { conflict, isLengthEven, union, isEven, getIdWithoutSet, loadImage, getImageSrc } from '../utils' import { insertAfter, operateClassName } from '../utils/domManipulate.js' import { tokenizer } from './parse' import { validEmoji } from '../emojis' +// for test +window.tokenizer = tokenizer + const snabbdom = require('snabbdom') const patch = snabbdom.init([ // Init patch function with chosen modules require('snabbdom/modules/class').default, // makes it easy to toggle classes @@ -45,11 +48,15 @@ class StateRender { return outerClass || (this.checkConflicted(block, token, cursor) ? CLASS_OR_ID['AG_GRAY'] : CLASS_OR_ID['AG_HIDE']) } + getHighlightClassName (active) { + return active ? CLASS_OR_ID['AG_HIGHLIGHT'] : CLASS_OR_ID['AG_SELECTION'] + } + /** * [render]: 2 steps: * render vdom */ - render (blocks, cursor, activeBlocks) { + render (blocks, cursor, activeBlocks, matches) { const selector = `${LOWERCASE_TAGS.div}#${CLASS_OR_ID['AG_EDITOR_ID']}` const renderBlock = block => { @@ -115,8 +122,10 @@ class StateRender { return h(blockSelector, data, block.children.map(child => renderBlock(child))) } else { + // highlight search key in block + const highlights = matches.filter(m => m.key === block.key) let children = block.text - ? tokenizer(block.text).reduce((acc, token) => { + ? tokenizer(block.text, highlights).reduce((acc, token) => { const chunk = this[token.type](h, cursor, block, token) return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk] }, []) @@ -184,111 +193,212 @@ class StateRender { } hr (h, cursor, block, token, outerClass) { + const { start, end } = token.range + const content = this.highlight(h, block, start, end, token) return [ - h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker) + h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, content) ] } header (h, cursor, block, token, outerClass) { const className = this.getClassName(outerClass, block, token, cursor) + const { start, end } = token.range + const content = this.highlight(h, block, start, end, token) return [ - h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker) + h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, content) ] } ['code_fense'] (h, cursor, block, token, outerClass) { + const { start, end } = token.range + const { marker } = token + + const markerContent = this.highlight(h, block, start, start + marker.length, token) + const content = this.highlight(h, block, start + marker.length, end, token) + return [ - h(`span.${CLASS_OR_ID['AG_GRAY']}`, token.marker), - h(`span.${CLASS_OR_ID['AG_LANGUAGE']}`, token.content) + h(`span.${CLASS_OR_ID['AG_GRAY']}`, markerContent), + h(`span.${CLASS_OR_ID['AG_LANGUAGE']}`, content) ] } backlash (h, cursor, block, token, outerClass) { const className = this.getClassName(outerClass, block, token, cursor) + const { start, end } = token.range + const content = this.highlight(h, block, start, end, token) + return [ - h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker) + h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, content) ] } ['inline_code'] (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 - marker.length, end, token) + const content = this.highlight(h, block, start + marker.length, end - marker.length, token) + return [ - h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker), - h('code', token.content), - h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker) + h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, startMarker), + h('code', content), + h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, endMarker) ] } + // change text to highlight vdom + highlight (h, block, rStart, rEnd, token) { + const { text } = block + const { highlights } = token + let result = [] + let unions = [] + let pos = rStart - text (h, cursor, block, token) { - return token.content + if (highlights) { + for (const light of highlights) { + const un = union({ start: rStart, end: rEnd }, light) + if (un) unions.push(un) + } + } + + if (unions.length) { + for (const u of unions) { + const { start, end, active } = u + const className = this.getHighlightClassName(active) + + if (pos < start) { + result.push(text.substring(pos, start)) + } + + result.push(h(`span.${className}`, text.substring(start, end))) + pos = end + } + if (pos < rEnd) { + result.push(block.text.substring(pos, rEnd)) + } + } else { + result = [ text.substring(rStart, rEnd) ] + } + + return result } - + // render token of text type to vdom. + text (h, cursor, block, token) { + const { start, end } = token.range + return this.highlight(h, block, start, end, token) + } + // render token of emoji to vdom emoji (h, cursor, block, token, outerClass) { + const { start: rStart, end: rEnd } = token.range const className = this.getClassName(outerClass, block, token, cursor) const validation = validEmoji(token.content) const finalClass = validation ? className : CLASS_OR_ID['AG_WARN'] + const CONTENT_CLASSNAME = `span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKED_TEXT']}` + let startMarkerCN = `span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKER']}` + let endMarkerCN = startMarkerCN + let content = token.content + let pos = rStart + token.marker.length + + if (token.highlights && token.highlights.length) { + content = [] + for (const light of token.highlights) { + let { start, end, active } = light + const HIGHLIGHT_CLASSNAME = this.getHighlightClassName(active) + if (start === rStart) { + startMarkerCN += `.${HIGHLIGHT_CLASSNAME}` + start++ + } + if (end === rEnd) { + endMarkerCN += `.${HIGHLIGHT_CLASSNAME}` + end-- + } + if (pos < start) { + content.push(block.text.substring(pos, start)) + } + if (start < end) { + content.push(h(`span.${HIGHLIGHT_CLASSNAME}`, block.text.substring(start, end))) + } + pos = end + } + if (pos < rEnd - token.marker.length) { + content.push(block.text.substring(pos, rEnd - 1)) + } + } + const emojiVdom = validation - ? h(`span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKED_TEXT']}`, { dataset: { emoji: validation.emoji } }, token.content) - : h(`span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKED_TEXT']}`, token.content) + ? h(CONTENT_CLASSNAME, { + dataset: { + emoji: validation.emoji + } + }, content) + : h(CONTENT_CLASSNAME, content) return [ - h(`span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKER']}`, token.marker), + h(startMarkerCN, token.marker), emojiVdom, - h(`span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKER']}`, token.marker) + h(endMarkerCN, token.marker) ] } // render factory of `del`,`em`,`strong` delEmStrongFac (type, h, cursor, block, token, outerClass) { const className = this.getClassName(outerClass, block, token, cursor) + const COMMON_MARKER = `span.${className}.${CLASS_OR_ID['AG_REMOVE']}` + const { marker } = token + const { start, end } = token.range + const backlashStart = end - marker.length - token.backlash.length + const content = [ + ...token.children.reduce((acc, to) => { + const chunk = this[to.type](h, cursor, block, to, className) + return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk] + }, []), + ...this.backlashInToken(token.backlash, className, backlashStart, token) + ] + const startMarker = this.highlight(h, block, start, start + marker.length, token) + const endMarker = this.highlight(h, block, end - marker.length, end, token) if (isLengthEven(token.backlash)) { return [ - h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker), - h(type, [ - ...token.children.reduce((acc, to) => { - const chunk = this[to.type](h, cursor, block, to, className) - return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk] - }, []), - ...this.backlashInToken(token.backlash, className) - ]), - h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, token.marker) + h(COMMON_MARKER, startMarker), + h(type, content), + h(COMMON_MARKER, endMarker) ] } else { return [ - token.marker, - ...token.children.reduce((acc, to) => { - const chunk = this[to.type](h, cursor, block, to, className) - return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk] - }, []), - ...this.backlashInToken(token.backlash, className), - token.marker + ...startMarker, + ...content, + ...endMarker ] } } - - backlashInToken (backlashes, outerClass) { + // TODO HIGHLIGHT + backlashInToken (backlashes, outerClass, start, token) { + const { highlights = [] } = token const chunks = backlashes.split('') const len = chunks.length const result = [] let i for (i = 0; i < len; i++) { + const chunk = chunks[i] + const light = highlights.filter(light => union({ start: start + i, end: start + i + 1 }, light)) + let selector = 'span' + if (light.length) { + const className = this.getHighlightClassName(light[0].active) + selector += `.${className}` + } if (isEven(i)) { result.push( - h(`span.${outerClass}`, chunks[i]) + h(`${selector}.${outerClass}`, chunk) ) } else { result.push( - h(`span.${CLASS_OR_ID['AG_BACKLASH']}`, chunks[i]) + h(`${selector}.${CLASS_OR_ID['AG_BACKLASH']}`, chunk) ) } } - // result.push( - // h(`span.${CLASS_OR_ID['AG_BUG']}`) // the extral a tag for fix bug - // ) - return result } // I dont want operate dom directly, is there any better method? need help! @@ -296,6 +406,23 @@ class StateRender { const className = this.getClassName(outerClass, block, token, cursor) const imageClass = CLASS_OR_ID['AG_IMAGE_MARKED_TEXT'] + const { + start, + end + } = token.range + const titleContent = this.highlight(h, block, start, start + 2 + token.title.length, token) + const srcContent = this.highlight( + h, block, + start + 2 + token.title.length + token.backlash.first.length, + start + 2 + token.title.length + token.backlash.first.length + 2 + token.src.length, + token + ) + const firstBracketContent = this.highlight(h, block, start, start + 2, token) + const lastBracketContent = this.highlight(h, block, end - 1, end, token) + + const firstBacklashStart = start + 2 + token.title.length + const secondBacklashStart = end - 1 - token.backlash.second.length + if (isLengthEven(token.backlash.first) && isLengthEven(token.backlash.second)) { let id let isSuccess @@ -347,11 +474,11 @@ class StateRender { } const children = [ - `![${token.title}`, - ...this.backlashInToken(token.backlash.first, className), - `](${token.src}`, - ...this.backlashInToken(token.backlash.second, className), - ')' + ...titleContent, + ...this.backlashInToken(token.backlash.first, className, firstBacklashStart, token), + ...srcContent, + ...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token), + ...lastBracketContent ] return isSuccess @@ -362,27 +489,29 @@ class StateRender { : [h(selector, children)] } else { return [ - '![', + ...firstBracketContent, ...token.children.reduce((acc, to) => { const chunk = this[to.type](h, cursor, block, to, className) return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk] }, []), - ...this.backlashInToken(token.backlash.first, className), - '](', - token.src, - ...this.backlashInToken(token.backlash.second, className), - ')' + ...this.backlashInToken(token.backlash.first, className, firstBacklashStart, token), + ...srcContent, + ...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token), + ...lastBracketContent ] } } - + // render auto_link to vdom ['auto_link'] (h, cursor, block, token, outerClass) { + const { start, end } = token.range + const content = this.highlight(h, block, start, end, token) + return [ h('a', { props: { href: token.href } - }, token.href) + }, content) ] } @@ -390,24 +519,50 @@ class StateRender { link (h, cursor, block, token, outerClass) { const className = this.getClassName(outerClass, block, token, cursor) const linkClassName = className === CLASS_OR_ID['AG_HIDE'] ? className : CLASS_OR_ID['AG_LINK_IN_BRACKET'] + const { start, end } = token.range + const firstMiddleBracket = this.highlight(h, block, start, start + 3, token) + + const firstBracket = this.highlight(h, block, start, start + 1, token) + const middleBracket = this.highlight( + h, block, + start + 1 + token.anchor.length + token.backlash.first.length, + start + 1 + token.anchor.length + token.backlash.first.length + 2, + token + ) + const hrefContent = this.highlight( + h, block, + start + 1 + token.anchor.length + token.backlash.first.length + 2, + start + 1 + token.anchor.length + token.backlash.first.length + 2 + token.href.length, + token + ) + const middleHref = this.highlight( + h, block, start + 1 + token.anchor.length + token.backlash.first.length, + block, start + 1 + token.anchor.length + token.backlash.first.length + 2 + token.href.length, + token + ) + + const lastBracket = this.highlight(h, block, end - 1, end, token) + + const firstBacklashStart = start + 1 + token.anchor.length + const secondBacklashStart = end - 1 - token.backlash.second.length if (isLengthEven(token.backlash.first) && isLengthEven(token.backlash.second)) { if (!token.children.length && !token.backlash.first) { // no-text-link return [ - h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, '[]('), + h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, firstMiddleBracket), h(`a.${CLASS_OR_ID['AG_NOTEXT_LINK']}`, { props: { href: token.href + encodeURI(token.backlash.second) } }, [ - token.href, - ...this.backlashInToken(token.backlash.second, className) + ...hrefContent, + ...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token) ]), - h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, ')') + h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, lastBracket) ] } else { // has children return [ - h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, '['), + h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, firstBracket), h('a', { dataset: { href: token.href + encodeURI(token.backlash.second) @@ -417,27 +572,27 @@ class StateRender { const chunk = this[to.type](h, cursor, block, to, className) return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk] }, []), - ...this.backlashInToken(token.backlash.first, className) + ...this.backlashInToken(token.backlash.first, className, firstBacklashStart, token) ]), - h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, ']('), + h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, middleBracket), h(`span.${linkClassName}.${CLASS_OR_ID['AG_REMOVE']}`, [ - token.href, - ...this.backlashInToken(token.backlash.second, className) + ...hrefContent, + ...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token) ]), - h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, ')') + h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, lastBracket) ] } } else { return [ - '[', + ...firstBracket, ...token.children.reduce((acc, to) => { const chunk = this[to.type](h, cursor, block, to, className) return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk] }, []), - ...this.backlashInToken(token.backlash.first, className), - `](${token.href}`, - ...this.backlashInToken(token.backlash.second, className), - ')' + ...this.backlashInToken(token.backlash.first, className, firstBacklashStart, token), + ...middleHref, + ...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token), + ...lastBracket ] } } diff --git a/src/editor/parser/parse.js b/src/editor/parser/parse.js index 8590a5f3..c3d1b425 100644 --- a/src/editor/parser/parse.js +++ b/src/editor/parser/parse.js @@ -1,5 +1,7 @@ import { beginRules, inlineRules } from './rules' -import { isLengthEven } from '../utils' +import { isLengthEven, union } from '../utils' + +const CAN_NEST_RULES = ['strong', 'em', 'link', 'del', 'image'] // image can not nest but it has children const tokenizerFac = (src, beginRules, inlineRules, pos = 0) => { const tokens = [] @@ -59,10 +61,11 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0) => { content: '', range: { start: pos, - end: pos + backTo[0].length + end: pos + backTo[1].length } }) pending += pending + backTo[2] + pendingStartPos = pos + backTo[1].length src = src.substring(backTo[0].length) pos = pos + backTo[0].length continue @@ -186,11 +189,34 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0) => { return tokens } -export const tokenizer = src => { - return tokenizerFac(src, beginRules, inlineRules, 0) +export const tokenizer = (src, highlights = []) => { + const tokens = tokenizerFac(src, beginRules, inlineRules, 0) + const postTokenizer = tokens => { + for (const token of tokens) { + for (const light of highlights) { + const highlight = union(token.range, light) + if (highlight) { + if (token.highlights && Array.isArray(token.highlights)) { + token.highlights.push(highlight) + } else { + token.highlights = [highlight] + } + } + } + if (CAN_NEST_RULES.indexOf(token.type) > -1) { + postTokenizer(token.children) + } + } + } + if (highlights.length) { + postTokenizer(tokens) + } + + return tokens } // transform `tokens` to text ignore the range of token +// the opposite of tokenizer export const generator = tokens => { let result = '' const getBash = bash => bash !== undefined ? bash : '' diff --git a/src/editor/utils/importMarkdown.js b/src/editor/utils/importMarkdown.js index a62b537a..f36b24f1 100644 --- a/src/editor/utils/importMarkdown.js +++ b/src/editor/utils/importMarkdown.js @@ -94,7 +94,7 @@ const importRegister = ContentState => { case 'h4': case 'h5': case 'h6': - const textValue = child.childNodes[0].value + const textValue = child.childNodes.length ? child.childNodes[0].value : '' const match = /\d/.exec(child.nodeName) value = match ? '#'.repeat(+match[0]) + textValue : textValue block = this.createBlock(child.nodeName, value) diff --git a/src/editor/utils/index.js b/src/editor/utils/index.js index 453e5b08..8cb8d133 100644 --- a/src/editor/utils/index.js +++ b/src/editor/utils/index.js @@ -45,6 +45,25 @@ export const conflict = (arr1, arr2) => { return !(arr1[1] < arr2[0] || arr2[1] < arr1[0]) } +export const union = ({ start: tStart, end: tEnd }, { start: lStart, end: lEnd, active }) => { + if (!(tEnd <= lStart || lEnd <= tStart)) { + if (lStart < tStart) { + return { + start: tStart, + end: tEnd < lEnd ? tEnd : lEnd, + active + } + } else { + return { + start: lStart, + end: tEnd < lEnd ? tEnd : lEnd, + active + } + } + } + return null +} + // https://github.com/jashkenas/underscore export const throttle = (func, wait = 50) => { let context diff --git a/src/main/menus/edit.js b/src/main/menus/edit.js index b8c8ac97..868b0b7c 100755 --- a/src/main/menus/edit.js +++ b/src/main/menus/edit.js @@ -32,5 +32,31 @@ export default { label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectall' + }, { + type: 'separator' + }, { + label: 'Find', + accelerator: 'CmdOrCtrl+F', + click: (menuItem, browserWindow) => { + actions.edit(browserWindow, 'find') + } + }, { + label: 'Find Next', + accelerator: 'Alt+CmdOrCtrl+U', + click: (menuItem, browserWindow) => { + actions.edit(browserWindow, 'fineNext') + } + }, { + label: 'FindPrev', + accelerator: 'Shift+CmdOrCtrl+U', + click: (menuItem, browserWindow) => { + actions.edit(browserWindow, 'findPrev') + } + }, { + label: 'Replace', + accelerator: 'Alt+CmdOrCtrl+F', + click: (menuItem, browserWindow) => { + actions.edit(browserWindow, 'replace') + } }] } diff --git a/src/renderer/App.vue b/src/renderer/App.vue index d11d7814..c7ac7f7a 100644 --- a/src/renderer/App.vue +++ b/src/renderer/App.vue @@ -6,19 +6,25 @@ :word-count="wordCount" > + \ No newline at end of file diff --git a/src/renderer/components/titleBar.vue b/src/renderer/components/titleBar.vue index 607fa7e7..5690bb53 100644 --- a/src/renderer/components/titleBar.vue +++ b/src/renderer/components/titleBar.vue @@ -1,6 +1,6 @@