diff --git a/inputRules.ts b/inputRules.ts index 4ceefe1..42c524d 100644 --- a/inputRules.ts +++ b/inputRules.ts @@ -1,747 +1,272 @@ import { SmartTypographySettings } from "types"; -import { ChangeSpec, Transaction } from "@codemirror/state"; - -const dashChar = "-"; -const enDashChar = "–"; -const emDashChar = "—"; - -interface InputRuleParams { - registerChange: (change: ChangeSpec, revert: ChangeSpec) => void; - adjustSelection: (adjustment: number) => void; - tr: Transaction; - settings: SmartTypographySettings; - fromA: number; - fromB: number; - context: string; -} export interface InputRule { trigger: string; - shouldReplace: (context: string) => boolean; - replace: (params: InputRuleParams) => void; -} - -function getLastChar(context: string) { - return context && context[context.length - 1]; + contextMatch: RegExp; + from: string; + to: string | ((settings: SmartTypographySettings) => string); } // Dashes - -export const enDash: InputRule = { - trigger: dashChar, - shouldReplace: (context: string) => { - return getLastChar(context) === dashChar; - }, - replace: ({ - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - registerChange( - { from: fromA - 1, to: fromA, insert: enDashChar }, - { - from: fromB - 1, - to: fromB, - insert: dashChar + dashChar, - } - ); - - adjustSelection(-1); +export const dashRules: InputRule[] = [ + // en dash + { + trigger: "-", + from: "--", + to: "–", + contextMatch: /-$/, + }, + // em dash + { + trigger: "-", + from: "–-", + to: "—", + contextMatch: /–$/, + }, + // tripple dash + { + trigger: "-", + from: "—-", + to: "---", + contextMatch: /—$/, }, -}; - -export const emDash: InputRule = { - trigger: dashChar, - shouldReplace: (context: string) => { - return getLastChar(context) === enDashChar; - }, - replace: ({ - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - registerChange( - { from: fromA - 1, to: fromA, insert: emDashChar }, - { - from: fromB - 1, - to: fromB, - insert: enDashChar + dashChar, - } - ); - - adjustSelection(-1); - }, -}; - -export const trippleDash: InputRule = { - trigger: dashChar, - shouldReplace: (context: string) => { - return getLastChar(context) === emDashChar; - }, - replace: ({ - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - registerChange( - { - from: fromA - 1, - to: fromA, - insert: dashChar + dashChar + dashChar, - }, - { - from: fromB - 1, - to: fromB + 2, - insert: emDashChar + dashChar, - } - ); +]; - adjustSelection(1); +export const dashRulesSansEnDash: InputRule[] = [ + // em dash + { + trigger: "-", + from: "--", + to: "—", + contextMatch: /-$/, + }, + // tripple dash + { + trigger: "-", + from: "—-", + to: "---", + contextMatch: /—$/, }, -}; - -export const dashRules = [enDash, emDash, trippleDash]; +]; // Ellipsis - -export const ellipsis: InputRule = { - trigger: ".", - shouldReplace: (context: string) => { - return context && context.endsWith(".."); - }, - replace: ({ - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - registerChange( - { from: fromA - 2, to: fromA, insert: "…" }, - { - from: fromB - 2, - to: fromB - 1, - insert: "...", - } - ); - - adjustSelection(-2); +export const ellipsisRules: InputRule[] = [ + { + trigger: ".", + from: "...", + to: "…", + contextMatch: /\.\.$/, }, -}; - -export const ellipsisRules = [ellipsis]; +]; // Quotes -export const doubleQuote: InputRule = { - trigger: '"', - shouldReplace: () => { - return true; - }, - replace: ({ - context, - registerChange, - settings, - fromA, - fromB, - }: InputRuleParams) => { - if (context.length === 0 || /[\s\{\[\(\<'"\u2018\u201C]$/.test(context)) { - registerChange( - { - from: fromA, - to: fromA, - insert: settings.openDouble, - }, - { from: fromB, to: fromB + 1, insert: '"' } - ); - } else { - registerChange( - { - from: fromA, - to: fromA, - insert: settings.closeDouble, - }, - { from: fromB, to: fromB + 1, insert: '"' } - ); - } - }, -}; - -export const pairedDoubleQuote: InputRule = { - trigger: '""', - shouldReplace: () => { - return true; - }, - replace: ({ registerChange, settings, fromA, fromB }: InputRuleParams) => { - registerChange( - { - from: fromA, - to: fromA, - insert: settings.openDouble + settings.closeDouble, - }, - { from: fromB, to: fromB + 2, insert: '""' } - ); - }, -}; - -export const singleQuote: InputRule = { - trigger: "'", - shouldReplace: () => { - return true; - }, - replace: ({ - registerChange, - settings, - fromA, - fromB, - context, - }: InputRuleParams) => { - if (context.length === 0 || /[\s\{\[\(\<'"\u2018\u201C]$/.test(context)) { - registerChange( - { - from: fromA, - to: fromA, - insert: settings.openSingle, - }, - { from: fromB, to: fromB + 1, insert: "'" } - ); - } else { - registerChange( - { - from: fromA, - to: fromA, - insert: settings.closeSingle, - }, - { from: fromB, to: fromB + 1, insert: "'" } - ); - } - }, -}; - -export const pairedSingleQuote: InputRule = { - trigger: "''", - shouldReplace: () => { - return true; - }, - replace: ({ registerChange, settings, fromA, fromB }: InputRuleParams) => { - registerChange( - { - from: fromA, - to: fromA, - insert: settings.openSingle + settings.closeSingle, - }, - { from: fromB, to: fromB + 2, insert: "''" } - ); - }, -}; - -export const smartQuoteRules = [ - doubleQuote, - pairedDoubleQuote, - singleQuote, - pairedSingleQuote, +export const smartQuoteRules: InputRule[] = [ + // Open double + { + trigger: '"', + from: '"', + to: (settings) => settings.openDouble, + contextMatch: /[\s\{\[\(\<'"\u2018\u201C]$/, + }, + // Close double + { + trigger: '"', + from: '"', + to: (settings) => settings.closeDouble, + contextMatch: /.*$/, + }, + // Paired double + { + trigger: '""', + from: '""', + to: (settings) => settings.openDouble + settings.closeDouble, + contextMatch: /.*$/, + }, + // Open single + { + trigger: "'", + from: "'", + to: (settings) => settings.openSingle, + contextMatch: /[\s\{\[\(\<'"\u2018\u201C]$/, + }, + // Close single + { + trigger: "'", + from: "'", + to: (settings) => settings.closeSingle, + contextMatch: /.*$/, + }, + // Paired single + { + trigger: "''", + from: "''", + to: (settings) => settings.openSingle + settings.closeSingle, + contextMatch: /.*$/, + }, ]; // Arrows - -export const leftArrow: InputRule = { - trigger: dashChar, - shouldReplace: (context: string) => { - return getLastChar(context) === "<"; - }, - replace: ({ - settings, - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - registerChange( - { - from: fromA - 1, - to: fromA, - insert: settings.leftArrow, - }, - { - from: fromB - 1, - to: fromB, - insert: "<" + dashChar, - } - ); - - adjustSelection(-1); - }, -}; - -export const rightArrow: InputRule = { - trigger: ">", - shouldReplace: (context: string) => { - return getLastChar(context) === dashChar; - }, - replace: ({ - settings, - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - registerChange( - { - from: fromA - 1, - to: fromA, - insert: settings.rightArrow, - }, - { - from: fromB - 1, - to: fromB, - insert: dashChar + ">", - } - ); - - adjustSelection(-1); +export const arrowRules: InputRule[] = [ + { + trigger: "-", + from: "<-", + to: (settings) => settings.leftArrow, + contextMatch: /<$/, + }, + { + trigger: ">", + from: "->", + to: (settings) => settings.rightArrow, + contextMatch: /-$/, }, -}; - -export const arrowRules = [leftArrow, rightArrow]; +]; // Guillemet - -export const leftGuillemet: InputRule = { - trigger: "<", - shouldReplace: (context: string) => { - return getLastChar(context) === "<"; - }, - replace: ({ - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - registerChange( - { from: fromA - 1, to: fromA, insert: "«" }, - { - from: fromB - 1, - to: fromB, - insert: "<<", - } - ); - - adjustSelection(-1); +export const guillemetRules: InputRule[] = [ + { + trigger: "<", + from: "<<", + to: "«", + contextMatch: /<$/, + }, + { + trigger: ">", + from: ">>", + to: "»", + contextMatch: />$/, }, -}; - -export const rightGuillemet: InputRule = { - trigger: ">", - shouldReplace: (context: string) => { - return getLastChar(context) === ">"; - }, - replace: ({ - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - registerChange( - { from: fromA - 1, to: fromA, insert: "»" }, - { - from: fromB - 1, - to: fromB, - insert: ">>", - } - ); - - adjustSelection(-1); - }, -}; - -export const guillemetRules = [leftGuillemet, rightGuillemet]; - -export const greaterThanOrEqualTo: InputRule = { - trigger: "=", - shouldReplace: (context: string) => { - return getLastChar(context) === ">"; - }, - replace: ({ - settings, - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - registerChange( - { - from: fromA - 1, - to: fromA, - insert: "≤", - }, - { - from: fromB - 1, - to: fromB, - insert: ">=", - } - ); - - adjustSelection(-1); - }, -}; - -export const lessThanOrEqualTo: InputRule = { - trigger: "=", - shouldReplace: (context: string) => { - return getLastChar(context) === "<"; - }, - replace: ({ - settings, - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - registerChange( - { - from: fromA - 1, - to: fromA, - insert: "≥", - }, - { - from: fromB - 1, - to: fromB, - insert: "<=", - } - ); - - adjustSelection(-1); - }, -}; - -export const notEqualTo: InputRule = { - trigger: "=", - shouldReplace: (context: string) => { - return getLastChar(context) === "/"; - }, - replace: ({ - settings, - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - registerChange( - { - from: fromA - 1, - to: fromA, - insert: "≠", - }, - { - from: fromB - 1, - to: fromB, - insert: "/=", - } - ); +]; - adjustSelection(-1); +// Comparisons +export const comparisonRules: InputRule[] = [ + { + trigger: "=", + from: ">=", + to: "≥", + contextMatch: />$/, + }, + { + trigger: "=", + from: "<=", + to: "≤", + contextMatch: /<$/, + }, + { + trigger: "=", + from: "/=", + to: "≠", + contextMatch: /\/$/, }, -}; - -export const comparisonRules = [ - lessThanOrEqualTo, - greaterThanOrEqualTo, - notEqualTo, ]; // Fractions - -export const frac2: InputRule = { - trigger: "2", - shouldReplace: (context: string) => { - return context && /(?:^|\s)1\/$/.test(context); - }, - replace: ({ - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - registerChange( - { from: fromA - 2, to: fromA, insert: "½" }, - { - from: fromB - 2, - to: fromB - 1, - insert: "1/2", - } - ); - - adjustSelection(-2); - }, -}; - -export const frac3: InputRule = { - trigger: "3", - shouldReplace: (context: string) => { - return context && /(?:^|\s)[12]\/$/.test(context); - }, - replace: ({ - context, - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - let insert = "⅓"; - let revert = "1/3"; - - if (context.endsWith("2/")) { - insert = "⅔"; - revert = "2/3"; - } - - registerChange( - { from: fromA - 2, to: fromA, insert }, - { - from: fromB - 2, - to: fromB - 1, - insert: revert, - } - ); - - adjustSelection(-2); - }, -}; - -export const frac4: InputRule = { - trigger: "4", - shouldReplace: (context: string) => { - return context && /(?:^|\s)[13]\/$/.test(context); - }, - replace: ({ - context, - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - let insert = "¼"; - let revert = "1/4"; - - if (context.endsWith("3/")) { - insert = "¾"; - revert = "3/4"; - } - - registerChange( - { from: fromA - 2, to: fromA, insert }, - { - from: fromB - 2, - to: fromB - 1, - insert: revert, - } - ); - - adjustSelection(-2); - }, -}; - -export const frac5: InputRule = { - trigger: "5", - shouldReplace: (context: string) => { - return context && /(?:^|\s)[1234]\/$/.test(context); - }, - replace: ({ - context, - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - let insert = "⅕"; - let revert = "1/5"; - - if (context.endsWith("2/")) { - insert = "⅖"; - revert = "2/5"; - } else if (context.endsWith("3/")) { - insert = "⅗"; - revert = "3/5"; - } else if (context.endsWith("4/")) { - insert = "⅘"; - revert = "4/5"; - } - - registerChange( - { from: fromA - 2, to: fromA, insert }, - { - from: fromB - 2, - to: fromB - 1, - insert: revert, - } - ); - - adjustSelection(-2); - }, -}; - -export const frac6: InputRule = { - trigger: "6", - shouldReplace: (context: string) => { - return context && /(?:^|\s)[15]\/$/.test(context); - }, - replace: ({ - context, - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - let insert = "⅙"; - let revert = "1/6"; - - if (context.endsWith("5/")) { - insert = "⅚"; - revert = "5/6"; - } - - registerChange( - { from: fromA - 2, to: fromA, insert }, - { - from: fromB - 2, - to: fromB - 1, - insert: revert, - } - ); - - adjustSelection(-2); +export const fractionRules: InputRule[] = [ + { + trigger: "2", + from: "1/2", + to: "½", + contextMatch: /(?:^|\s)1\/$/, + }, + { + trigger: "3", + from: "1/3", + to: "⅓", + contextMatch: /(?:^|\s)1\/$/, + }, + { + trigger: "3", + from: "2/3", + to: "⅔", + contextMatch: /(?:^|\s)2\/$/, + }, + { + trigger: "4", + from: "1/4", + to: "¼", + contextMatch: /(?:^|\s)1\/$/, + }, + { + trigger: "4", + from: "3/4", + to: "¾", + contextMatch: /(?:^|\s)3\/$/, + }, + { + trigger: "5", + from: "1/5", + to: "⅕", + contextMatch: /(?:^|\s)1\/$/, + }, + { + trigger: "5", + from: "2/5", + to: "⅖", + contextMatch: /(?:^|\s)2\/$/, + }, + { + trigger: "5", + from: "3/5", + to: "⅗", + contextMatch: /(?:^|\s)3\/$/, + }, + { + trigger: "5", + from: "4/5", + to: "⅘", + contextMatch: /(?:^|\s)4\/$/, + }, + { + trigger: "6", + from: "1/6", + to: "⅙", + contextMatch: /(?:^|\s)1\/$/, + }, + { + trigger: "6", + from: "5/6", + to: "⅚", + contextMatch: /(?:^|\s)5\/$/, + }, + { + trigger: "7", + from: "1/7", + to: "⅐", + contextMatch: /(?:^|\s)1\/$/, + }, + { + trigger: "8", + from: "1/8", + to: "⅛", + contextMatch: /(?:^|\s)1\/$/, + }, + { + trigger: "8", + from: "3/8", + to: "⅜", + contextMatch: /(?:^|\s)3\/$/, + }, + { + trigger: "8", + from: "5/8", + to: "⅝", + contextMatch: /(?:^|\s)5\/$/, + }, + { + trigger: "8", + from: "7/8", + to: "⅞", + contextMatch: /(?:^|\s)7\/$/, + }, + { + trigger: "9", + from: "1/9", + to: "⅑", + contextMatch: /(?:^|\s)1\/$/, + }, + { + trigger: "0", + from: "1/10", + to: "⅒", + contextMatch: /(?:^|\s)1\/1$/, }, -}; - -export const frac7: InputRule = { - trigger: "7", - shouldReplace: (context: string) => { - return context && /(?:^|\s)1\/$/.test(context); - }, - replace: ({ - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - let insert = "⅐"; - let revert = "1/7"; - - registerChange( - { from: fromA - 2, to: fromA, insert }, - { - from: fromB - 2, - to: fromB - 1, - insert: revert, - } - ); - - adjustSelection(-2); - }, -}; - -export const frac8: InputRule = { - trigger: "8", - shouldReplace: (context: string) => { - return context && /(?:^|\s)[1357]\/$/.test(context); - }, - replace: ({ - context, - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - let insert = "⅛"; - let revert = "1/8"; - - if (context.endsWith("3/")) { - insert = "⅜"; - revert = "3/8"; - } else if (context.endsWith("5/")) { - insert = "⅝"; - revert = "5/8"; - } else if (context.endsWith("7/")) { - insert = "⅞"; - revert = "7/8"; - } - - registerChange( - { from: fromA - 2, to: fromA, insert }, - { - from: fromB - 2, - to: fromB - 1, - insert: revert, - } - ); - - adjustSelection(-2); - }, -}; - -export const frac9: InputRule = { - trigger: "9", - shouldReplace: (context: string) => { - return context && /(?:^|\s)1\/$/.test(context); - }, - replace: ({ - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - let insert = "⅑"; - let revert = "1/9"; - - registerChange( - { from: fromA - 2, to: fromA, insert }, - { - from: fromB - 2, - to: fromB - 1, - insert: revert, - } - ); - - adjustSelection(-2); - }, -}; - -export const frac10: InputRule = { - trigger: "0", - shouldReplace: (context: string) => { - return context && /(?:^|\s)1\/1$/.test(context); - }, - replace: ({ - registerChange, - adjustSelection, - fromA, - fromB, - }: InputRuleParams) => { - let insert = "⅒"; - let revert = "1/10"; - - registerChange( - { from: fromA - 3, to: fromA, insert }, - { - from: fromB - 3, - to: fromB - 2, - insert: revert, - } - ); - - adjustSelection(-3); - }, -}; - -export const fractionRules = [ - frac2, - frac3, - frac4, - frac5, - frac6, - frac7, - frac8, - frac9, - frac10, ]; diff --git a/legacyInputRules.ts b/legacyInputRules.ts index 7413322..f01da28 100644 --- a/legacyInputRules.ts +++ b/legacyInputRules.ts @@ -222,13 +222,11 @@ export const greaterThanOrEqualTo: LegacyInputRule = { matchRegExp: />=$/, performUpdate: (instance, delta, settings) => { delta.update({ line: delta.from.line, ch: delta.from.ch - 1 }, delta.to, [ - settings.greaterThanOrEqualTo, + "≥", ]); }, performRevert: (instance, delta, settings) => { - if ( - instance.getRange(delta.from, delta.to) === settings.greaterThanOrEqualTo - ) { + if (instance.getRange(delta.from, delta.to) === "≥") { delta.update(delta.from, delta.to, [">="]); } }, @@ -239,13 +237,11 @@ export const lessThanOrEqualTo: LegacyInputRule = { matchRegExp: /<=$/, performUpdate: (instance, delta, settings) => { delta.update({ line: delta.from.line, ch: delta.from.ch - 1 }, delta.to, [ - settings.lessThanOrEqualTo, + "≤", ]); }, performRevert: (instance, delta, settings) => { - if ( - instance.getRange(delta.from, delta.to) === settings.lessThanOrEqualTo - ) { + if (instance.getRange(delta.from, delta.to) === "≤") { delta.update(delta.from, delta.to, ["<="]); } }, @@ -256,11 +252,11 @@ export const notEqualTo: LegacyInputRule = { matchRegExp: /\/=$/, performUpdate: (instance, delta, settings) => { delta.update({ line: delta.from.line, ch: delta.from.ch - 1 }, delta.to, [ - settings.notEqualTo, + "≠", ]); }, performRevert: (instance, delta, settings) => { - if (instance.getRange(delta.from, delta.to) === settings.notEqualTo) { + if (instance.getRange(delta.from, delta.to) === "≠") { delta.update(delta.from, delta.to, ["/="]); } }, diff --git a/main.ts b/main.ts index 8d4e233..58dc939 100644 --- a/main.ts +++ b/main.ts @@ -1,15 +1,5 @@ -import { - legacyArrowRules, - legacyEllipsisRules, - legacyDashRules, - LegacyInputRule, - legacySmartQuoteRules, - legacyGuillemetRules, - legacyComparisonRules, -} from "legacyInputRules"; import { App, Plugin, PluginSettingTab, Setting } from "obsidian"; import { - ChangeSet, ChangeSpec, EditorSelection, EditorState, @@ -19,12 +9,22 @@ import { } from "@codemirror/state"; import { syntaxTree } from "@codemirror/language"; import { tokenClassNodeProp } from "@codemirror/stream-parser"; -import { SmartTypographySettings } from "types"; import { Tree } from "@lezer/common"; +import { SmartTypographySettings } from "types"; +import { + legacyArrowRules, + legacyEllipsisRules, + legacyDashRules, + LegacyInputRule, + legacySmartQuoteRules, + legacyGuillemetRules, + legacyComparisonRules, +} from "legacyInputRules"; import { arrowRules, comparisonRules, dashRules, + dashRulesSansEnDash, ellipsisRules, fractionRules, guillemetRules, @@ -40,6 +40,7 @@ const DEFAULT_SETTINGS: SmartTypographySettings = { comparisons: true, fractions: false, guillemets: false, + skipEnDash: false, openSingle: "‘", closeSingle: "’", @@ -54,6 +55,7 @@ const DEFAULT_SETTINGS: SmartTypographySettings = { export default class SmartTypography extends Plugin { settings: SmartTypographySettings; inputRules: InputRule[]; + inputRuleMap: Record; legacyInputRules: LegacyInputRule[]; legacyLastUpdate: WeakMap; @@ -61,9 +63,15 @@ export default class SmartTypography extends Plugin { buildInputRules() { this.legacyInputRules = []; this.inputRules = []; + this.inputRuleMap = {}; if (this.settings.emDash) { - this.inputRules.push(...dashRules); + if (this.settings.skipEnDash) { + this.inputRules.push(...dashRulesSansEnDash); + } else { + this.inputRules.push(...dashRules); + } + this.legacyInputRules.push(...legacyDashRules); } @@ -95,11 +103,17 @@ export default class SmartTypography extends Plugin { if (this.settings.fractions) { this.inputRules.push(...fractionRules); } + + this.inputRules.forEach((rule) => { + if (this.inputRuleMap[rule.trigger] === undefined) { + this.inputRuleMap[rule.trigger] = []; + } + + this.inputRuleMap[rule.trigger].push(rule); + }); } async onload() { - this.legacyLastUpdate = new WeakMap(); - await this.loadSettings(); this.addSettingTab(new SmartTypographySettingTab(this.app, this)); @@ -152,14 +166,9 @@ export default class SmartTypography extends Plugin { return tr; } - // Cache the syntax tree if we end up having to access it + // Cache the syntax tree if we end up accessing it let tree: Tree = null; - const getTree = () => { - if (!tree) tree = syntaxTree(tr.state); - return tree; - }; - // Memoize any positions we check so we can avoid some work const seenPositions: Record = {}; @@ -168,7 +177,9 @@ export default class SmartTypography extends Plugin { return seenPositions[pos]; } - const nodeProps = getTree() + if (!tree) tree = syntaxTree(tr.state); + + const nodeProps = tree .resolveInner(pos, 1) .type.prop(tokenClassNodeProp); @@ -181,15 +192,6 @@ export default class SmartTypography extends Plugin { return seenPositions[pos]; }; - let selection = tr.selection; - - const adjustSelection = (adjustment: number) => { - const ranges = selection.ranges.map((r) => - EditorSelection.range(r.anchor + adjustment, r.head + adjustment) - ); - selection = EditorSelection.create(ranges); - }; - // Store a list of changes and specs to revert these changes const changes: ChangeSpec[] = []; const reverts: ChangeSpec[] = []; @@ -200,14 +202,17 @@ export default class SmartTypography extends Plugin { }; const contextCache: Record = {}; + let newSelection = tr.selection; tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => { const insertedText = inserted.sliceString(0, 0 + inserted.length); + const matchedRules = this.inputRuleMap[insertedText]; - for (let rule of this.inputRules) { - // This character is not a trigger, so continue testing rules - if (insertedText !== rule.trigger) continue; + if (!matchedRules) { + return; + } + for (let rule of matchedRules) { // If we're in a codeblock, etc, return early, no need to continue checking if (!canPerformReplacement(fromA)) return; @@ -216,18 +221,43 @@ export default class SmartTypography extends Plugin { contextCache[fromA] = tr.newDoc.sliceString(fromB - 3, fromB); } - // Final check: given the context, see if we should perform a replacement - if (rule.shouldReplace(contextCache[fromA])) { - return rule.replace({ - adjustSelection, - context: contextCache[fromA], - fromA, - fromB, - registerChange, - settings: this.settings, - tr, - }); + const context = contextCache[fromA]; + + if (!rule.contextMatch.test(context)) { + continue; } + + const insert = + typeof rule.to === "string" ? rule.to : rule.to(this.settings); + const replacementLength = rule.from.length - rule.trigger.length; + const insertionPoint = fromA - replacementLength; + const reversionPoint = fromB - replacementLength; + + registerChange( + { + from: insertionPoint, + to: insertionPoint + replacementLength, + insert, + }, + { + from: reversionPoint, + to: reversionPoint + insert.length, + insert: rule.from, + } + ); + + const selectionAdjustment = rule.from.length - insert.length; + + newSelection = EditorSelection.create( + newSelection.ranges.map((r) => + EditorSelection.range( + r.anchor - selectionAdjustment, + r.head - selectionAdjustment + ) + ) + ); + + return; } }, false); @@ -241,7 +271,7 @@ export default class SmartTypography extends Plugin { scrollIntoView: tr.scrollIntoView, changes: reverts, }), - selection: selection, + selection: newSelection, scrollIntoView: tr.scrollIntoView, changes, }, @@ -253,6 +283,7 @@ export default class SmartTypography extends Plugin { ]); // Codemirror 5 + this.legacyLastUpdate = new WeakMap(); this.registerCodeMirror((cm: CodeMirror.Editor) => { cm.on("beforeChange", this.beforeChangeHandler); }); @@ -452,6 +483,20 @@ class SmartTypographySettingTab extends PluginSettingTab { }); }); + new Setting(containerEl) + .setName("Skip en-dash") + .setDesc( + "When enabled, two dashes will be converted to an em-dash rather than an en-dash." + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.skipEnDash) + .onChange(async (value) => { + this.plugin.settings.skipEnDash = value; + await this.plugin.saveSettings(); + }); + }); + new Setting(containerEl) .setName("Ellipsis") .setDesc("Three periods (...) will be converted to an ellipses (…)") diff --git a/manifest.json b/manifest.json index b78cf08..de27077 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-smart-typography", "name": "Smart Typography", - "version": "1.0.11", + "version": "1.0.12", "minAppVersion": "0.13.8", "description": "Converts quotes to curly quotes, dashes to em dashes, and periods to ellipses", "author": "mgmeyers", diff --git a/types.ts b/types.ts index 557f35a..3a34211 100644 --- a/types.ts +++ b/types.ts @@ -6,6 +6,7 @@ export interface SmartTypographySettings { guillemets: boolean; comparisons: boolean; fractions: boolean; + skipEnDash: boolean; openSingle: string; closeSingle: string; diff --git a/versions.json b/versions.json index 30d6afd..e72e0a2 100644 --- a/versions.json +++ b/versions.json @@ -1,7 +1,5 @@ { "1.0.11": "0.13.8", - "1.0.10": "0.13.8", - "1.0.9": "0.13.8", "1.0.8": "0.12.19", "1.0.1": "0.9.12", "1.0.0": "0.9.7"