Skip to content

Commit 972e3a7

Browse files
committed
Added option to use PrismJS in editing mode. This fixes #82, #134 and #140
1 parent 4a541fe commit 972e3a7

10 files changed

Lines changed: 423 additions & 69 deletions

File tree

main.js

Lines changed: 60 additions & 60 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EditorView/EditorExtensions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { annotationsExtension } from "./Annotations";
1818
import { executeCodeExtension } from "./ExecuteCodePlugin";
1919
import { admonitionExtension } from "./Admonitions";
2020
import { wrapExtension } from "./Wrapping";
21+
import { prismHighlightExtension } from "./PrismHighlight";
2122

2223

2324
export function extensions(plugin: CodeBlockCustomizerPlugin, settings: CodeblockCustomizerSettings) {
@@ -36,6 +37,7 @@ export function extensions(plugin: CodeBlockCustomizerPlugin, settings: Codebloc
3637
const { annotationViewPlugin } = annotationsExtension(plugin, settings, codeBlockPositionsField);
3738
const { executeCodeViewPlugin } = executeCodeExtension(plugin, settings, codeBlockPositionsField);
3839
const { admonitionViewPlugin } = admonitionExtension(plugin, settings, codeBlockPositionsField);
40+
const { prismHighlightPlugin } = prismHighlightExtension(plugin, settings, codeBlockPositionsField);
3941

4042
/* Extensions */
4143

@@ -85,6 +87,7 @@ export function extensions(plugin: CodeBlockCustomizerPlugin, settings: Codebloc
8587
hideFencesPlugin,
8688
executeCodeViewPlugin,
8789
admonitionViewPlugin,
90+
prismHighlightPlugin,
8891
scrollSyncPlugin,
8992
liveUpdateExtension(),
9093
forceRefreshListener,

src/EditorView/InlineCode.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getLanguageIcon, isSourceMode, addTextToClipboard, getInlineCodeIcon, g
66
import { CodeblockCustomizerSettings, InlineCodeModifierKeys } from "../Settings";
77
import { INLINE_CODE_LANG_REGEX } from "../Const";
88
import CodeBlockCustomizerPlugin from "../main";
9+
import { getPrismInstance, getTokenRangesFromPrismHighlighedtHTML } from "./PrismHighlight";
910

1011
export function inlineCodeExtension(plugin: CodeBlockCustomizerPlugin, settings: CodeblockCustomizerSettings) {
1112
const inlineCodeViewPlugin = ViewPlugin.fromClass(class {
@@ -72,16 +73,30 @@ export function inlineCodeExtension(plugin: CodeBlockCustomizerPlugin, settings:
7273
}
7374
}
7475

75-
const tokens = getCM5Tokens(code, langName);
76-
let currentPos = codeStartPos;
77-
for (const token of tokens) {
78-
if (token.style) {
79-
const classes = token.style.split(' ').map(s => `cm-${s}`).join(' ');
80-
if (token.text.length > 0) {
81-
decorations.push(Decoration.mark({ class: classes }).range(currentPos, currentPos + token.text.length));
76+
const prism = settings.pluginSettings.codeblock.usePrismHighlight ? getPrismInstance() : null;
77+
const tokenRanges = prism ? getTokenRangesFromPrismHighlighedtHTML(code, langName) : null;
78+
if (tokenRanges) {
79+
decorations.push(Decoration.mark({ class: "cbc-prism-inline" }).range(node.from, node.to));
80+
81+
for (const range of tokenRanges) {
82+
const from = codeStartPos + range.from;
83+
const to = codeStartPos + range.to;
84+
if (from < to && to <= node.to) {
85+
decorations.push(Decoration.mark({ class: range.classes }).range(from, to));
8286
}
8387
}
84-
currentPos += token.text.length;
88+
} else {
89+
const tokens = getCM5Tokens(code, langName);
90+
let currentPos = codeStartPos;
91+
for (const token of tokens) {
92+
if (token.style) {
93+
const classes = token.style.split(' ').map(s => `cm-${s}`).join(' ');
94+
if (token.text.length > 0) {
95+
decorations.push(Decoration.mark({ class: classes }).range(currentPos, currentPos + token.text.length));
96+
}
97+
}
98+
currentPos += token.text.length;
99+
}
85100
}
86101
},
87102
});

src/EditorView/PrismHighlight.ts

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import { loadPrism } from "obsidian";
2+
3+
import { StateField, RangeSet, Range } from "@codemirror/state";
4+
import { EditorView, Decoration, DecorationSet, ViewPlugin, ViewUpdate } from "@codemirror/view";
5+
6+
import { CodeblockCustomizerSettings } from "../Settings";
7+
import CodeBlockCustomizerPlugin from "../main";
8+
import { getLanguageConfig, isSourceMode } from "../Utils";
9+
import { CodeBlockPositions, getVisibleCodeBlocks } from "./CodeBlockPositions";
10+
11+
export interface PrismTokenRange {
12+
from: number;
13+
to: number;
14+
classes: string;
15+
}
16+
17+
interface CachedBlock {
18+
content: string;
19+
language: string;
20+
tokenRanges: PrismTokenRange[];
21+
}
22+
23+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
24+
export function walkPrismTokens(tokens: any[], offset: number, result: PrismTokenRange[]): number {
25+
for (const token of tokens) {
26+
if (typeof token === "string") {
27+
offset += token.length;
28+
} else {
29+
const parts = ["token", token.type];
30+
if (token.alias) {
31+
if (Array.isArray(token.alias)) {
32+
parts.push(...token.alias);
33+
} else {
34+
parts.push(token.alias);
35+
}
36+
}
37+
38+
const classes = parts.join(" ");
39+
const start = offset;
40+
41+
if (Array.isArray(token.content)) {
42+
offset = walkPrismTokens(token.content, offset, result);
43+
} else if (typeof token.content === "string") {
44+
offset += token.content.length;
45+
} else {
46+
offset = walkPrismTokens([token.content], offset, result);
47+
}
48+
49+
if (offset > start) {
50+
result.push({ from: start, to: offset, classes });
51+
}
52+
}
53+
}
54+
return offset;
55+
}// walkPrismTokens
56+
57+
export function getTokenRangesFromPrismHighlighedtHTML(code: string, language: string): PrismTokenRange[] | null {
58+
if (!prismInstance || !prismInstance.languages[language]) {
59+
return null;
60+
}
61+
62+
const html = prismInstance.highlight(code, prismInstance.languages[language], language);
63+
const container = createSpan();
64+
container.innerHTML = html;
65+
66+
const result: PrismTokenRange[] = [];
67+
let offset = 0;
68+
69+
function walk(node: Node) {
70+
if (node.nodeType === Node.TEXT_NODE) {
71+
offset += (node.textContent ?? "").length;
72+
} else if (node.nodeType === Node.ELEMENT_NODE) {
73+
const el = node as HTMLElement;
74+
const classes = el.className;
75+
const start = offset;
76+
for (const child of Array.from(el.childNodes)) {
77+
walk(child);
78+
}
79+
if (classes && offset > start) {
80+
result.push({ from: start, to: offset, classes });
81+
}
82+
}
83+
}
84+
85+
for (const child of Array.from(container.childNodes)) {
86+
walk(child);
87+
}
88+
89+
return result;
90+
}// getTokenRangesFromPrismHighlighedtHTML
91+
92+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
93+
let prismInstance: any = null;
94+
let isPrismLoading = false;
95+
96+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
97+
export function getPrismInstance(): any {
98+
return prismInstance;
99+
}// getPrismInstance
100+
101+
export function ensurePrismLoaded(onLoaded?: () => void): void {
102+
if (prismInstance) {
103+
onLoaded?.();
104+
return;
105+
}
106+
107+
if (isPrismLoading) {
108+
return;
109+
}
110+
111+
isPrismLoading = true;
112+
113+
loadPrism().then(p => {
114+
prismInstance = p;
115+
isPrismLoading = false;
116+
onLoaded?.();
117+
});
118+
}// ensurePrismLoaded
119+
120+
export function prismHighlightExtension(plugin: CodeBlockCustomizerPlugin, settings: CodeblockCustomizerSettings, codeBlockPositionsField: StateField<CodeBlockPositions[]>) {
121+
122+
const cache = new Map<number, CachedBlock>();
123+
124+
const prismHighlightPlugin = ViewPlugin.fromClass(class {
125+
decorations: DecorationSet;
126+
lastVisibleBlockStarts: Set<number> = new Set();
127+
128+
constructor(view: EditorView) {
129+
this.decorations = Decoration.none;
130+
131+
if (prismInstance) {
132+
this.decorations = this.buildDecorations(view);
133+
} else {
134+
ensurePrismLoaded(() => view.dispatch());
135+
}
136+
}
137+
138+
update(update: ViewUpdate) {
139+
if (!prismInstance) {
140+
return;
141+
}
142+
143+
if (!settings.pluginSettings.codeblock.usePrismHighlight) {
144+
if (this.decorations !== Decoration.none) {
145+
this.decorations = Decoration.none;
146+
}
147+
return;
148+
}
149+
150+
if (!settings.pluginSettings.common.enableInSourceMode && isSourceMode(update.state))
151+
return;
152+
153+
const codeBlocksChanged = update.startState.field(codeBlockPositionsField, false) !== update.state.field(codeBlockPositionsField, false);
154+
if (update.docChanged || codeBlocksChanged || plugin.settingsUpdated) {
155+
this.decorations = this.buildDecorations(update.view);
156+
return;
157+
}
158+
159+
if (update.viewportChanged) {
160+
this.decorations = this.extendDecorations(update.view);
161+
return;
162+
}
163+
164+
// empty dispatch. required during loading
165+
if (this.decorations === Decoration.none) {
166+
this.decorations = this.buildDecorations(update.view);
167+
}
168+
}
169+
170+
extendDecorations(view: EditorView): DecorationSet {
171+
const positions = view.state.field(codeBlockPositionsField, false) ?? [];
172+
const visibleBlocks = getVisibleCodeBlocks(positions, view.visibleRanges);
173+
const newBlocks = visibleBlocks.filter(b => !this.lastVisibleBlockStarts.has(b.codeBlockStartPos));
174+
175+
if (newBlocks.length === 0) {
176+
return this.decorations;
177+
}
178+
179+
newBlocks.forEach(b => this.lastVisibleBlockStarts.add(b.codeBlockStartPos));
180+
181+
const newDecorations = this.buildDecorationsForBlocks(view, newBlocks);
182+
183+
return this.decorations.update({ add: newDecorations, sort: true });
184+
}// extendDecorations
185+
186+
buildDecorations(view: EditorView): DecorationSet {
187+
if (!settings.pluginSettings.common.enableInSourceMode && isSourceMode(view.state))
188+
return Decoration.none;
189+
190+
if (!settings.pluginSettings.codeblock.usePrismHighlight) {
191+
return Decoration.none;
192+
}
193+
194+
const positions = view.state.field(codeBlockPositionsField, false) ?? [];
195+
const visibleBlocks = getVisibleCodeBlocks(positions, view.visibleRanges);
196+
197+
this.lastVisibleBlockStarts = new Set(visibleBlocks.map(b => b.codeBlockStartPos));
198+
199+
const decorations = this.buildDecorationsForBlocks(view, visibleBlocks);
200+
201+
return RangeSet.of(decorations, true);
202+
}// buildDecorations
203+
204+
buildDecorationsForBlocks(view: EditorView, blocks: CodeBlockPositions[]): Range<Decoration>[] {
205+
const decorations: Range<Decoration>[] = [];
206+
207+
for (const block of blocks) {
208+
if (block.parameters.exclude) {
209+
continue;
210+
}
211+
212+
const firstLine = view.state.doc.lineAt(block.codeBlockStartPos);
213+
const lastLine = view.state.doc.lineAt(block.codeBlockEndPos);
214+
const codeStartLineNum = firstLine.number + 1;
215+
const codeEndLineNum = lastLine.number - 1;
216+
if (codeStartLineNum > codeEndLineNum) {
217+
continue;
218+
}
219+
220+
const codeStartPos = view.state.doc.line(codeStartLineNum).from;
221+
const codeEndPos = view.state.doc.line(codeEndLineNum).to;
222+
const code = view.state.sliceDoc(codeStartPos, codeEndPos);
223+
224+
let language = block.parameters.language?.toLowerCase() ?? "";
225+
// strip run- prefix
226+
if (language.startsWith("run-")) {
227+
language = language.substring(4);
228+
}
229+
230+
const customLangConfig = getLanguageConfig(language, plugin);
231+
const prismLanguage = customLangConfig?.format ?? language;
232+
if (!prismLanguage || !prismInstance.languages[prismLanguage]) {
233+
continue;
234+
}
235+
236+
let tokenRanges: PrismTokenRange[];
237+
const cached = cache.get(block.codeBlockStartPos);
238+
if (cached && cached.content === code && cached.language === prismLanguage) {
239+
tokenRanges = cached.tokenRanges;
240+
} else {
241+
const grammar = prismInstance.languages[prismLanguage];
242+
const tokens = prismInstance.tokenize(code, grammar);
243+
244+
tokenRanges = [];
245+
walkPrismTokens(tokens, 0, tokenRanges);
246+
247+
cache.set(block.codeBlockStartPos, {
248+
content: code,
249+
language: prismLanguage,
250+
tokenRanges,
251+
});
252+
}
253+
254+
for (const range of tokenRanges) {
255+
const from = codeStartPos + range.from;
256+
const to = codeStartPos + range.to;
257+
if (from >= to || to > view.state.doc.length) {
258+
continue;
259+
}
260+
261+
decorations.push(Decoration.mark({ class: range.classes }).range(from, to));
262+
}
263+
264+
for (let lineNum = codeStartLineNum; lineNum <= codeEndLineNum; lineNum++) {
265+
const linePos = view.state.doc.line(lineNum).from;
266+
decorations.push(Decoration.line({ class: "cbc-prism" }).range(linePos));
267+
}
268+
}
269+
270+
return decorations;
271+
}// buildDecorationsForBlocks
272+
273+
destroy() {
274+
cache.clear();
275+
}
276+
}, {
277+
decorations: v => v.decorations
278+
});
279+
280+
return { prismHighlightPlugin };
281+
}// prismHighlightExtension

src/Settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export interface PluginSettings {
135135
enableBracketHighlight: boolean;
136136
highlightNonMatchingBrackets: boolean;
137137
enableSelectionMatching: boolean;
138+
usePrismHighlight: boolean;
138139
unwrapcode: boolean;
139140
hideFenceLines: boolean;
140141
lineNumberSeparatorStyle: LineNumberSeparatorStyle;
@@ -264,6 +265,7 @@ const defaultThemeSettings: PluginSettings = {
264265
enableBracketHighlight: true,
265266
highlightNonMatchingBrackets: true,
266267
enableSelectionMatching: true,
268+
usePrismHighlight: false,
267269
unwrapcode: false,
268270
hideFenceLines: false,
269271
lineNumberSeparatorStyle: LineNumberSeparatorStyle.Dashed,

src/SettingsTab/Behavior.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,23 @@ export class BehaviorSettings {
5757
})
5858
);
5959

60-
// grouped code blocks
60+
const prismSetting = new Setting(behaviorDiv)
61+
.setName('Use PrismJS syntax highlighting in editor mode')
62+
.setDesc('If enabled, editor mode will use PrismJS for syntax highlighting instead of CodeMirror\'s built-in highlighting. This makes editor mode syntax highlighting match reading mode syntax highlighting.')
63+
.addToggle(toggle => toggle
64+
.setValue(this.plugin.settings.pluginSettings.codeblock.usePrismHighlight)
65+
.onChange(async (value) => {
66+
this.plugin.settings.pluginSettings.codeblock.usePrismHighlight = value;
67+
await this.plugin.saveSettings();
68+
})
69+
);
70+
const warningEl = prismSetting.descEl.createDiv({ cls: "mod-warning" });
71+
warningEl.style.color = "var(--text-error)";
72+
warningEl.style.marginTop = "4px";
73+
warningEl.style.fontWeight = "bold";
74+
warningEl.setText("Experimental: This feature is still being tested. Please report any issues you encounter.");
75+
76+
// grouped code blocks
6177
const groupedCodeBlocksDetails = createDetailsGroup(behaviorDiv, 'Grouped Code Block Settings', 'groupedCodeBlocksDetailsOpen', this, this.getSearchQuery);
6278

6379
new Setting(groupedCodeBlocksDetails)

0 commit comments

Comments
 (0)