Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/website/documentation/notes/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ The following shortcuts work in **Editor** and **Live Preview** modes:
- <kbd>Cmd+B</kbd> / <kbd>Ctrl+B</kbd> for **bold**
- <kbd>Cmd+I</kbd> / <kbd>Ctrl+I</kbd> for *italic*
- <kbd>Cmd+Shift+S</kbd> / <kbd>Ctrl+Shift+S</kbd> for ~~strikethrough~~
- <kbd>Cmd+Shift+H</kbd> / <kbd>Ctrl+Shift+H</kbd> for <span style="background-color: yellow;color:black;padding:1px 2px;">highlight</span>

Press the same shortcut again to remove the markdown markers from the current selection.

Expand Down
3 changes: 2 additions & 1 deletion src/renderer/components/notes/NotesEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { createInternalLinks } from './cm-extensions/internalLinks'
import { createListIndent } from './cm-extensions/listIndent'
import { createListLineIndent } from './cm-extensions/listLineIndent'
import { createMarkdownDecorations } from './cm-extensions/markdownDecorations'
import { Highlight } from './cm-extensions/markdownHighlight'
import { markdownShortcuts } from './cm-extensions/markdownShortcuts'
import { createMermaidBlocks } from './cm-extensions/mermaidBlocks'
import { moveSelectionToAdjacentMermaidSource } from './cm-extensions/mermaidNavigation'
Expand Down Expand Up @@ -191,7 +192,7 @@ function createEditorState(doc: string): EditorState {
markdown({
base: markdownLanguage,
codeLanguages: languages,
extensions: GFM,
extensions: [GFM, Highlight],
}),
createCodeHighlight(isDark.value),
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
import { syntaxTree } from '@codemirror/language'
import { languages } from '@codemirror/language-data'
import { EditorState } from '@codemirror/state'
import { GFM } from '@lezer/markdown'
import { describe, expect, it } from 'vitest'
import { Highlight } from '../markdownHighlight'

function createState(doc: string) {
return EditorState.create({
doc,
extensions: [
markdown({
base: markdownLanguage,
codeLanguages: languages,
extensions: [GFM, Highlight],
}),
],
})
}

function findNodes(state: EditorState, name: string) {
const result: { from: number, to: number, text: string }[] = []
syntaxTree(state).iterate({
enter(node) {
if (node.name === name) {
result.push({
from: node.from,
to: node.to,
text: state.sliceDoc(node.from, node.to),
})
}
},
})
return result
}

describe('highlight inline parser', () => {
it('parses ==text== into a Highlight node with two HighlightMark children', () => {
const state = createState('==hello==')
const highlights = findNodes(state, 'Highlight')
const marks = findNodes(state, 'HighlightMark')

expect(highlights).toEqual([{ from: 0, to: 9, text: '==hello==' }])
expect(marks.map(m => m.text)).toEqual(['==', '=='])
})

it('parses highlight inside paragraph text', () => {
const state = createState('foo ==bar== baz')
const highlights = findNodes(state, 'Highlight')

expect(highlights).toEqual([{ from: 4, to: 11, text: '==bar==' }])
})

it('does not parse single = pair as highlight', () => {
const state = createState('=hello=')
expect(findNodes(state, 'Highlight')).toEqual([])
})

it('does not parse three or more equals as highlight', () => {
const state = createState('===hello===')
expect(findNodes(state, 'Highlight')).toEqual([])
})

it('does not parse runs of equals adjacent to delimiters', () => {
const state = createState('====text====')
expect(findNodes(state, 'Highlight')).toEqual([])
})

it('does not parse opener with whitespace immediately after', () => {
const state = createState('a == b == c')
expect(findNodes(state, 'Highlight')).toEqual([])
})

it('does not parse highlight inside inline code', () => {
const state = createState('`==hello==`')
expect(findNodes(state, 'Highlight')).toEqual([])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,25 @@ describe('toggleInlineMarkdown', () => {
text: 'hello ~~world~~',
})
})

it('wraps selected text with highlight markers', () => {
expect(toggleInlineMarkdown('hello world', 6, 11, '==')).toEqual({
selection: {
from: 8,
to: 13,
},
text: 'hello ==world==',
})
})
})

describe('markdownShortcuts', () => {
it('registers bold, italic, and strikethrough shortcuts', () => {
it('registers bold, italic, strikethrough, and highlight shortcuts', () => {
expect(markdownShortcuts.map(binding => binding.key)).toEqual([
'Mod-b',
'Mod-i',
'Mod-Shift-s',
'Mod-Shift-h',
])
})
})
1 change: 1 addition & 0 deletions src/renderer/components/notes/cm-extensions/hideMarkup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const HIDEABLE_MARKS = new Set([
'HeaderMark',
'EmphasisMark',
'StrikethroughMark',
'HighlightMark',
'CodeMark',
'CodeInfo',
'LinkMark',
Expand Down
12 changes: 12 additions & 0 deletions src/renderer/components/notes/cm-extensions/markdownDecorations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,18 @@ function buildDecorations(
)
}

// Highlight (==text==)
if (type === 'Highlight') {
decorations.push(
Decoration.mark({
attributes: {
style:
'background:var(--text-highlight);color:#1f2937;border-radius:3px;padding:0 2px',
},
}).range(node.from, node.to),
)
}

// Inline code
if (type === 'InlineCode') {
decorations.push(
Expand Down
46 changes: 46 additions & 0 deletions src/renderer/components/notes/cm-extensions/markdownHighlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { MarkdownConfig } from '@lezer/markdown'
import { tags } from '@lezer/highlight'

const Punctuation = /[\p{S}\p{P}]/u

const HighlightDelim = { resolve: 'Highlight', mark: 'HighlightMark' }

export const Highlight: MarkdownConfig = {
defineNodes: [
{
name: 'Highlight',
style: { 'Highlight/...': tags.special(tags.content) },
},
{
name: 'HighlightMark',
style: tags.processingInstruction,
},
],
parseInline: [
{
name: 'Highlight',
parse(cx, next, pos) {
if (next !== 61 || cx.char(pos + 1) !== 61)
return -1
if (cx.char(pos - 1) === 61 || cx.char(pos + 2) === 61)
return -1

const before = cx.slice(pos - 1, pos)
const after = cx.slice(pos + 2, pos + 3)
const sBefore = /\s|^$/.test(before)
const sAfter = /\s|^$/.test(after)
const pBefore = Punctuation.test(before)
const pAfter = Punctuation.test(after)

return cx.addDelimiter(
HighlightDelim,
pos,
pos + 2,
!sAfter && (!pAfter || sBefore || pBefore),
!sBefore && (!pBefore || sAfter || pAfter),
)
},
after: 'Emphasis',
},
],
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,8 @@ export const markdownShortcuts: KeyBinding[] = [
key: 'Mod-Shift-s',
run: createInlineMarkdownCommand('~~'),
},
{
key: 'Mod-Shift-h',
run: createInlineMarkdownCommand('=='),
},
]