Skip to content

Commit cfbd402

Browse files
committed
feat(commands): magic formatting: magic reference & magic tag commands
1 parent f57881d commit cfbd402

File tree

2 files changed

+141
-59
lines changed

2 files changed

+141
-59
lines changed

src/app.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
import { improveCursorMovementFeature, improveSearchFeature, spareBlocksFeature } from './features'
2525
import { borderView, columnsView, galleryView, hideDotRefs, tabularView } from './views'
2626
import { getChosenBlocks, p, scrollToBlock } from './utils'
27-
import { magicCode, magicHighlight, magicItalics, magicStrikethrough, magicUnderline } from './commands/magic_markup'
27+
import { magicCode, magicHighlight, magicItalics, magicRef, magicStrikethrough, magicTag, magicUnderline } from './commands/magic_markup'
2828

2929

3030
const DEV = process.env.NODE_ENV === 'development'
@@ -473,6 +473,16 @@ async function main() {
473473
// @ts-expect-error
474474
keybinding: {},
475475
}, (e) => updateBlocksCommand(magicCode, false, false))
476+
logseq.App.registerCommandPalette({
477+
label: ICON + ' Magic [[reference]]', key: 'mc-7-update-20-magic-ref',
478+
// @ts-expect-error
479+
keybinding: {},
480+
}, (e) => updateBlocksCommand(magicRef, false, false))
481+
logseq.App.registerCommandPalette({
482+
label: ICON + ' Magic #tag', key: 'mc-7-update-21-magic-tag',
483+
// @ts-expect-error
484+
keybinding: {},
485+
}, (e) => updateBlocksCommand(magicTag, false, false))
476486

477487

478488
// Navigation

src/commands/magic_markup.ts

Lines changed: 130 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,115 @@ import '@logseq/libs'
22
import { BlockEntity } from '@logseq/libs/dist/LSPlugin'
33

44

5-
type MarkUp = [string, string][] & {wrappedWith?: string}
5+
class MarkUp implements Iterable<[string, string]> {
6+
public wrap: [string, string]
7+
public alternativeWrap: [string, string]
8+
public unwrap: [string, string][]
9+
10+
public wrappedWith?: string
11+
public alternativeWrapWhenMatch?: RegExp
12+
13+
constructor({ wrap, unwrap, alternativeWrapWhenMatch, alternativeWrapIndex }: {
14+
wrap: [string, string]
15+
unwrap: ([string, string] | null)[]
16+
alternativeWrapWhenMatch?: RegExp
17+
alternativeWrapIndex?: number
18+
}) {
19+
this.wrap = wrap
20+
this.unwrap = unwrap.map((item) => item === null ? wrap : item)
21+
if (!this.unwrap.includes(wrap))
22+
this.unwrap.splice(0, 0, wrap)
23+
this.wrappedWith = undefined
24+
this.alternativeWrapWhenMatch = alternativeWrapWhenMatch
25+
this.alternativeWrap = this.unwrap[alternativeWrapIndex ?? 1]
26+
}
27+
28+
[Symbol.iterator]() {
29+
let index = 0
30+
return {
31+
next: () => {
32+
return {
33+
done: index >= this.unwrap.length,
34+
value: this.unwrap[index++],
35+
}
36+
}
37+
}
38+
}
39+
getWrapFor(selection: string) {
40+
if (this.alternativeWrapWhenMatch && this.alternativeWrapWhenMatch.test(selection))
41+
return this.alternativeWrap
42+
return this.wrap
43+
}
44+
getUnwrapFor(line: string, selectPosition: [number, number]): [string, string] | null {
45+
// Assertion: selectPosition should be already trimmed, so markup is always OUTside
46+
47+
for (const markup of this.unwrap)
48+
if (MarkUp.isMarkedUpWith(line, selectPosition, markup))
49+
return markup
50+
return null
51+
}
52+
static isMarkedUpWith(line: string, selectPosition: [number, number], markup: [string, string]): boolean {
53+
const [start, end] = selectPosition
54+
const [frontMarkup, backMarkup] = markup
55+
56+
if (start < frontMarkup.length)
57+
return false
58+
if (end + backMarkup.length > line.length)
59+
return false
60+
61+
const charsBefore = line.slice(start - frontMarkup.length, start)
62+
const charsAfter = line.slice(end, end + backMarkup.length)
63+
return charsBefore === frontMarkup && charsAfter === backMarkup
64+
}
65+
}
66+
667
const MARKUP: {[type: string]: MarkUp } = {
7-
// first pair is for wrapping, others — for unwrapping
8-
bold: [['**', '**'], ['<b>', '</b>']],
9-
italics: [['_', '_'], ['*', '*'], ['<i>', '</i>']],
10-
strikethrough: [['~~', '~~'], ['<s>', '</s>']],
11-
highlight: [['==', '=='], ['<mark>', '</mark>']],
12-
underline: [['<ins>', '</ins>'], ['<u>', '</u>']],
13-
code: [['`', '`'], ['<code>', '</code>']],
68+
bold: new MarkUp({
69+
wrap: ['**', '**'],
70+
unwrap: [['<b>', '</b>']] }),
71+
italics: new MarkUp({
72+
wrap: ['_', '_'],
73+
unwrap: [['*', '*'], ['<i>', '</i>']] }),
74+
strikethrough: new MarkUp({
75+
wrap: ['~~', '~~'],
76+
unwrap: [['<s>', '</s>']] }),
77+
highlight: new MarkUp({
78+
wrap: ['==', '=='],
79+
unwrap: [['<mark>', '</mark>']] }),
80+
underline: new MarkUp({
81+
wrap: ['<ins>', '</ins>'],
82+
unwrap: [['<u>', '</u>']] }),
83+
code: new MarkUp({
84+
wrap: ['`', '`'],
85+
unwrap: [['<code>', '</code>']] }),
86+
ref: new MarkUp({
87+
wrap: ['[[', ']]'],
88+
unwrap: [['#[[', ']]'], null, ['#', '']] }),
89+
tag: new MarkUp({
90+
wrap: ['#', ''],
91+
unwrap: [['#[[', ']]'], ['[[', ']]']],
92+
alternativeWrapWhenMatch: /\s+/,
93+
alternativeWrapIndex: 1, }),
1494
}
95+
type MARKUP_NAME = keyof typeof MARKUP
1596

1697
const TRIM_BEGIN = [
17-
/^\s+/,
98+
/^\s+/, // spaces at the beginning
1899

19-
/^#{1,6} /,
100+
/^#{1,6} /, // headings
20101

21-
/^\s*[-+*] /,
22-
/^\s*[-+*] \[.\] /,
23-
/^>/,
102+
/^\s*[-+*] /, // list item
103+
/^\s*[-+*] \[.\] /, // list item with markdown task
104+
/^>/, // quote
24105

106+
// logseq tasks
25107
/^(LATER|TODO) (\[#(A|B|C)\])?/,
26108
/^(NOW|DOING) (\[#(A|B|C)\])?/,
27109
/^DONE (\[#(A|B|C)\])?/,
28110
/^(WAIT|WAITING) (\[#(A|B|C)\])?/,
29111
/^(CANCELED|CANCELLED) (\[#(A|B|C)\])?/,
30-
/^\[#(A|B|C)\]/,
112+
113+
/^\[#(A|B|C)\]/, // non-task blocks with priorities
31114

32115
/^\s*[^\s:;,^@#~"`/|\\(){}[\]]+:: /u, // property without value
33116
/^\s*DEADLINE: <[^>]+>$/,
@@ -36,20 +119,18 @@ const TRIM_BEGIN = [
36119
const TRIM_BEGIN_SELECTION_ADDITION = [
37120
/^\s*[^\s:;,^@#~"`/|\\(){}[\]]+:: .*$/u, // property with value
38121
]
39-
40122
const TRIM_BEFORE: (string | RegExp)[] = [
41123
/^\(\([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\)\)/, // block ref
42124
/^{{\w+.*?}}/, // macro call
43125

44126
/^\s/,
45127
]
46-
47128
const TRIM_AFTER: (string | RegExp)[] = [
48129
/\!\[.*?\]\(.+?\){.*?}$/, // link to image
49130
/\!\[.*?\]\(.+?\)$/, // link to image
50131

51-
// /\]\(.+?\)$/, // link to page
52-
/\]\(\[\[.+?\]\]\)$/, // link to page
132+
// /\]\(.+?\)$/, // link to page
133+
/\]\(\[\[.+?\]\]\)$/, // link to page
53134

54135
/\]\(\(\([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\)\)\)$/, // link to block
55136
/\(\([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\)\)$/, // block ref
@@ -60,7 +141,6 @@ const TRIM_AFTER: (string | RegExp)[] = [
60141

61142
/\]\($/, // to not break markdown links
62143
]
63-
64144
const EXPAND_WHEN_OUTSIDE = [
65145
// this is OR groups
66146
// order is expansion direction
@@ -116,6 +196,9 @@ function trim(line: string, markup: MarkUp, selectPosition: [number, number], is
116196
while (true) {
117197
let wasTrimmed = false
118198
trimBefore.forEach(strOrRE => {
199+
if (!strOrRE)
200+
return
201+
119202
if (typeof strOrRE === 'string') {
120203
if (selection.startsWith(strOrRE)) {
121204
selection = selection.slice(strOrRE.length)
@@ -139,6 +222,9 @@ function trim(line: string, markup: MarkUp, selectPosition: [number, number], is
139222
while (true) {
140223
let wasTrimmed = false
141224
trimAfter.forEach((strOrRE) => {
225+
if (!strOrRE)
226+
return
227+
142228
if (typeof strOrRE === 'string') {
143229
if (selection.endsWith(strOrRE)) {
144230
selection = selection.slice(0, -strOrRE.length)
@@ -162,29 +248,6 @@ function trim(line: string, markup: MarkUp, selectPosition: [number, number], is
162248
selectPosition[1] = end
163249
}
164250

165-
function isMarkedUp(line: string, markup: MarkUp, selectPosition: [number, number]) {
166-
// note: may change markup
167-
// assertion:: selectPosition should be already trimmed, so markup is always OUTside
168-
169-
const [start, end] = selectPosition
170-
for (const [i, [frontMarkup, backMarkup]] of Object.entries(markup)) {
171-
if (start < frontMarkup.length)
172-
continue
173-
if (end + backMarkup.length > line.length)
174-
continue
175-
176-
const charsBefore = line.slice(start - frontMarkup.length, start)
177-
const charsAfter = line.slice(end, end + backMarkup.length)
178-
if (charsBefore === frontMarkup && charsAfter === backMarkup) {
179-
markup.wrappedWith = i
180-
Object.defineProperty(markup, 'wrappedWith', {enumerable: false})
181-
return true
182-
}
183-
}
184-
185-
return false
186-
}
187-
188251
function wordAtPosition(line: string, position: number) {
189252
const wordLeftRegexp = /(?!_)[\p{Letter}\p{Number}'_-]*$/u
190253
const wordRightRegexp = /^[\p{Letter}\p{Number}'_-]*(?<!_)/u
@@ -199,7 +262,7 @@ function wordAtPosition(line: string, position: number) {
199262
function expand(line: string, markup: MarkUp, selectPosition: [number, number]) {
200263
let [start, end] = selectPosition
201264

202-
let wordEdge
265+
let wordEdge: number
203266
[start, wordEdge] = wordAtPosition(line, start)
204267
if (wordEdge < end)
205268
[wordEdge, end] = wordAtPosition(line, end)
@@ -229,7 +292,7 @@ function expand(line: string, markup: MarkUp, selectPosition: [number, number])
229292

230293
const trimLastSpace = Boolean(pair[2])
231294

232-
if (isMarkedUp(line, [[L, R]] as MarkUp, [start, end])) {
295+
if (MarkUp.isMarkedUpWith(line, [start, end], [L, R])) {
233296
start -= L.length
234297
end += R.length
235298
setSelection()
@@ -239,10 +302,11 @@ function expand(line: string, markup: MarkUp, selectPosition: [number, number])
239302
})
240303
}
241304

242-
function applyMarkup(line: string, markup: MarkUp, selectPosition?: [number, number]): string {
305+
function applyMarkup(line: string, markupName: MARKUP_NAME, selectPosition?: [number, number]): string {
243306
if (!line && !selectPosition)
244307
return ''
245308

309+
const markup = MARKUP[markupName]
246310

247311
const isSelectionMode = Boolean(selectPosition)
248312
if (!selectPosition)
@@ -262,23 +326,23 @@ function applyMarkup(line: string, markup: MarkUp, selectPosition?: [number, num
262326
}
263327

264328
let selection = line.slice(start, end)
265-
266-
if (isMarkedUp(line, markup, selectPosition)) {
267-
const [frontMarkup, backMarkup] = markup[markup.wrappedWith!]
329+
const wrappedWith = markup.getUnwrapFor(line, selectPosition)
330+
if (wrappedWith) {
331+
const [frontMarkup, backMarkup] = wrappedWith
268332
selectPosition[0] -= frontMarkup.length
269333
selectPosition[1] -= frontMarkup.length
270334
return line.slice(0, start - frontMarkup.length) + selection + line.slice(end + backMarkup.length)
271335
} else {
272-
const [frontMarkup, backMarkup] = markup[0]
336+
const [frontMarkup, backMarkup] = markup.getWrapFor(selection)
273337
selectPosition[0] += frontMarkup.length
274338
selectPosition[1] += frontMarkup.length
275339
return line.slice(0, start) + frontMarkup + selection + backMarkup + line.slice(end)
276340
}
277341
}
278342

279-
function wrap(block: BlockEntity, content: string, markup: MarkUp) {
343+
function wrap(block: BlockEntity, content: string, markupName: MARKUP_NAME) {
280344
if (!block._selectPosition)
281-
return content.split('\n').map((line) => applyMarkup(line, markup)).join('\n')
345+
return content.split('\n').map((line) => applyMarkup(line, markupName)).join('\n')
282346

283347
let lineStartPosition = 0
284348
let newLineStartPosition = 0
@@ -316,7 +380,7 @@ function wrap(block: BlockEntity, content: string, markup: MarkUp) {
316380
Math.max(selectStart - lineStartPosition, 0),
317381
Math.min(selectEnd - lineStartPosition, line.length),
318382
]
319-
const newLine = applyMarkup(line, markup, wholeLine ? undefined : selectPositionLine)
383+
const newLine = applyMarkup(line, markupName, wholeLine ? undefined : selectPositionLine)
320384

321385
if (!wholeLine) {
322386
// first line of selection
@@ -338,25 +402,33 @@ function wrap(block: BlockEntity, content: string, markup: MarkUp) {
338402
}
339403

340404
export function magicBold(content, level, block, parent) {
341-
return wrap(block, content, MARKUP.bold)
405+
return wrap(block, content, 'bold me')
342406
}
343407

344408
export function magicItalics(content, level, block, parent) {
345-
return wrap(block, content, MARKUP.italics)
409+
return wrap(block, content, 'italics')
346410
}
347411

348412
export function magicStrikethrough(content, level, block, parent) {
349-
return wrap(block, content, MARKUP.strikethrough)
413+
return wrap(block, content, 'strikethrough')
350414
}
351415

352416
export function magicHighlight(content, level, block, parent) {
353-
return wrap(block, content, MARKUP.highlight)
417+
return wrap(block, content, 'highlight')
354418
}
355419

356420
export function magicUnderline(content, level, block, parent) {
357-
return wrap(block, content, MARKUP.underline)
421+
return wrap(block, content, 'underline')
358422
}
359423

360424
export function magicCode(content, level, block, parent) {
361-
return wrap(block, content, MARKUP.code)
425+
return wrap(block, content, 'code')
426+
}
427+
428+
export function magicRef(content, level, block, parent) {
429+
return wrap(block, content, 'ref')
430+
}
431+
432+
export function magicTag(content, level, block, parent) {
433+
return wrap(block, content, 'tag')
362434
}

0 commit comments

Comments
 (0)