create better structure inspired by ObsidianTweaks'
This commit is contained in:
297
src/BetterFormatting.ts
Normal file
297
src/BetterFormatting.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import {
|
||||
App,
|
||||
Editor,
|
||||
EditorChange,
|
||||
EditorPosition,
|
||||
EditorRange,
|
||||
EditorRangeOrCaret,
|
||||
EditorSelection,
|
||||
EditorTransaction,
|
||||
MarkdownView,
|
||||
} from 'obsidian'
|
||||
import BindThemPlugin from './main'
|
||||
import { getMainSelection, selectionToRange } from './Utils'
|
||||
import { PRE_WIDOW_EXPAND_CHARS, POST_WIDOW_EXPAND_CHARS } from './Constants'
|
||||
|
||||
/**
|
||||
* BetterFormatting provides enhanced markdown formatting toggling
|
||||
* that intelligently expands selections to word boundaries and
|
||||
* handles widow characters.
|
||||
*/
|
||||
export class BetterFormatting {
|
||||
public app: App
|
||||
private plugin: BindThemPlugin
|
||||
|
||||
constructor(app: App, plugin: BindThemPlugin) {
|
||||
this.app = app
|
||||
this.plugin = plugin
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a range by words, including intelligent handling of widow characters
|
||||
*/
|
||||
private expandRangeByWords(
|
||||
editor: Editor,
|
||||
range: EditorRange,
|
||||
symbolStart: string,
|
||||
symbolEnd: string
|
||||
): EditorRange {
|
||||
// Expand to word boundaries
|
||||
let from = (editor.wordAt(range.from) || range).from
|
||||
let to = (editor.wordAt(range.to) || range).to
|
||||
|
||||
// Include leading 'widows' (characters that should be included before the word)
|
||||
while (true) {
|
||||
const newFrom: EditorPosition = {
|
||||
line: from.line,
|
||||
ch: from.ch - 1,
|
||||
}
|
||||
|
||||
const newTo: EditorPosition = {
|
||||
line: to.line,
|
||||
ch: to.ch + 1,
|
||||
}
|
||||
|
||||
const preChar = editor.getRange(newFrom, from)
|
||||
const postChar = editor.getRange(to, newTo)
|
||||
|
||||
// Don't do this if widows form a matched pair
|
||||
if (preChar === postChar) {
|
||||
break
|
||||
}
|
||||
|
||||
if (symbolStart.endsWith(preChar)) {
|
||||
break
|
||||
}
|
||||
|
||||
if (!PRE_WIDOW_EXPAND_CHARS.includes(preChar)) {
|
||||
break
|
||||
}
|
||||
|
||||
from = newFrom
|
||||
}
|
||||
|
||||
// Include trailing 'widows'
|
||||
while (true) {
|
||||
const newFrom: EditorPosition = {
|
||||
line: from.line,
|
||||
ch: from.ch - 1,
|
||||
}
|
||||
|
||||
const newTo: EditorPosition = {
|
||||
line: to.line,
|
||||
ch: to.ch + 1,
|
||||
}
|
||||
|
||||
const preChar = editor.getRange(newFrom, from)
|
||||
const postChar = editor.getRange(to, newTo)
|
||||
|
||||
// Don't do this if widows form a matched pair
|
||||
if (preChar === postChar) {
|
||||
break
|
||||
}
|
||||
|
||||
if (symbolEnd.startsWith(postChar)) {
|
||||
break
|
||||
}
|
||||
|
||||
if (!POST_WIDOW_EXPAND_CHARS.includes(postChar)) {
|
||||
break
|
||||
}
|
||||
|
||||
to = newTo
|
||||
}
|
||||
|
||||
// Include leading symbolStart
|
||||
const newFrom = { line: from.line, ch: from.ch - symbolStart.length }
|
||||
const preString = editor.getRange(newFrom, from)
|
||||
if (preString === symbolStart) {
|
||||
from = newFrom
|
||||
}
|
||||
|
||||
// Include following symbolEnd
|
||||
const newTo = { line: to.line, ch: to.ch + symbolEnd.length }
|
||||
const postString = editor.getRange(to, newTo)
|
||||
if (postString === symbolEnd) {
|
||||
to = newTo
|
||||
}
|
||||
|
||||
return {
|
||||
from: from,
|
||||
to: to,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if text in the given range is already wrapped with the specified symbols
|
||||
*/
|
||||
private isWrapped(
|
||||
editor: Editor,
|
||||
range: EditorRange,
|
||||
symbolStart: string,
|
||||
symbolEnd: string
|
||||
): boolean {
|
||||
const text = editor.getRange(range.from, range.to)
|
||||
return text.startsWith(symbolStart) && text.endsWith(symbolEnd)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create changes to wrap text with symbols
|
||||
*/
|
||||
private wrap(
|
||||
editor: Editor,
|
||||
textRange: EditorRange,
|
||||
selection: EditorSelection,
|
||||
symbolStart: string,
|
||||
symbolEnd: string
|
||||
): [Array<EditorChange>, EditorSelection] {
|
||||
const changes: Array<EditorChange> = [
|
||||
{
|
||||
from: textRange.from,
|
||||
text: symbolStart,
|
||||
},
|
||||
{
|
||||
from: textRange.to,
|
||||
text: symbolEnd,
|
||||
},
|
||||
]
|
||||
|
||||
const newSelection: EditorSelection = {
|
||||
anchor: { ...selection.anchor },
|
||||
head: { ...selection.head },
|
||||
}
|
||||
newSelection.anchor.ch += symbolStart.length
|
||||
newSelection.head.ch += symbolEnd.length
|
||||
|
||||
return [changes, newSelection]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create changes to unwrap text from symbols
|
||||
*/
|
||||
private unwrap(
|
||||
editor: Editor,
|
||||
textRange: EditorRange,
|
||||
selection: EditorSelection,
|
||||
symbolStart: string,
|
||||
symbolEnd: string
|
||||
): [Array<EditorChange>, EditorSelection] {
|
||||
const changes: Array<EditorChange> = [
|
||||
{
|
||||
from: textRange.from,
|
||||
to: {
|
||||
line: textRange.from.line,
|
||||
ch: textRange.from.ch + symbolStart.length,
|
||||
},
|
||||
text: '',
|
||||
},
|
||||
{
|
||||
from: {
|
||||
line: textRange.to.line,
|
||||
ch: textRange.to.ch - symbolEnd.length,
|
||||
},
|
||||
to: textRange.to,
|
||||
text: '',
|
||||
},
|
||||
]
|
||||
|
||||
const newSelection: EditorSelection = {
|
||||
anchor: { ...selection.anchor },
|
||||
head: { ...selection.head },
|
||||
}
|
||||
newSelection.anchor.ch -= symbolStart.length
|
||||
newSelection.head.ch -= symbolStart.length
|
||||
|
||||
return [changes, newSelection]
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the wrap state for a selection
|
||||
*/
|
||||
private setSelectionWrapState(
|
||||
editor: Editor,
|
||||
selection: EditorSelection,
|
||||
wrapState: boolean,
|
||||
symbolStart: string,
|
||||
symbolEnd: string
|
||||
): [Array<EditorChange>, EditorSelection] {
|
||||
const initialRange = selectionToRange(selection)
|
||||
const textRange: EditorRange = this.expandRangeByWords(
|
||||
editor,
|
||||
initialRange,
|
||||
symbolStart,
|
||||
symbolEnd
|
||||
)
|
||||
|
||||
// Check if already wrapped
|
||||
const alreadyWrapped = this.isWrapped(editor, textRange, symbolStart, symbolEnd)
|
||||
if (alreadyWrapped === wrapState) {
|
||||
return [[], selection]
|
||||
}
|
||||
|
||||
// Wrap or unwrap
|
||||
if (wrapState) {
|
||||
return this.wrap(editor, textRange, selection, symbolStart, symbolEnd)
|
||||
}
|
||||
|
||||
return this.unwrap(editor, textRange, selection, symbolStart, symbolEnd)
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle wrapper symbols around the current selection(s)
|
||||
*
|
||||
* Principle: Toggling twice == no-op
|
||||
*/
|
||||
public toggleWrapper(
|
||||
editor: Editor,
|
||||
view: MarkdownView,
|
||||
symbolStart: string,
|
||||
symbolEnd?: string
|
||||
): void {
|
||||
if (symbolEnd === undefined) {
|
||||
symbolEnd = symbolStart
|
||||
}
|
||||
|
||||
const selections = editor.listSelections()
|
||||
const mainSelection = getMainSelection(editor)
|
||||
|
||||
// Get wrapped state of main selection
|
||||
const mainRange: EditorRange = this.expandRangeByWords(
|
||||
editor,
|
||||
selectionToRange(mainSelection),
|
||||
symbolStart,
|
||||
symbolEnd
|
||||
)
|
||||
|
||||
// Check if already wrapped
|
||||
const isWrapped = this.isWrapped(editor, mainRange, symbolStart, symbolEnd)
|
||||
const targetWrapState = !isWrapped
|
||||
|
||||
// Process all selections
|
||||
const newChanges: Array<EditorChange> = []
|
||||
const newSelectionRanges: Array<EditorRangeOrCaret> = []
|
||||
|
||||
for (const selection of selections) {
|
||||
const [changes, newSelection] = this.setSelectionWrapState(
|
||||
editor,
|
||||
selection,
|
||||
targetWrapState,
|
||||
symbolStart,
|
||||
symbolEnd
|
||||
)
|
||||
|
||||
newChanges.push(...changes)
|
||||
newSelectionRanges.push(selectionToRange(newSelection))
|
||||
}
|
||||
|
||||
const transaction: EditorTransaction = {
|
||||
changes: newChanges,
|
||||
selections: newSelectionRanges,
|
||||
}
|
||||
|
||||
let origin = targetWrapState ? '+' : '-'
|
||||
origin += 'BetterFormatting_' + symbolStart + symbolEnd
|
||||
|
||||
editor.transaction(transaction, origin)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user