Files
plainleaf/client/codemirror/fenced_code.ts
T
Zef Hemel dbcef9e8aa Fix: make sure Edit buttons work even for Lua expressions and code
widgets whose bodies appear multiple times in the same page (previously
always jumped to first).
2026-03-12 14:13:18 +01:00

104 lines
3.4 KiB
TypeScript

import type { EditorState } from "@codemirror/state";
import { syntaxTree } from "@codemirror/language";
import { Decoration } from "@codemirror/view";
import type { Client } from "../client.ts";
import {
decoratorStateField,
invisibleDecoration,
isCursorInRange,
shouldRenderWidgets,
} from "./util.ts";
import { IFrameWidget } from "./iframe_widget.ts";
export function fencedCodePlugin(client: Client) {
return decoratorStateField((state: EditorState) => {
const widgets: any[] = [];
syntaxTree(state).iterate({
enter({ from, to, name, node }) {
if (name === "FencedCode") {
if (isCursorInRange(state, [from, to])) {
// Don't render the widget if the cursor is inside the fenced code
return;
}
const text = state.sliceDoc(from, to);
const [_, lang] = text.match(/^(?:```+|~~~+)(\w+)?/)!;
const codeWidgetCallback =
client.clientSystem.codeWidgetHook.codeWidgetCallbacks.get(lang);
// Only custom render when we have a custom renderer, and the current page is not a template
if (codeWidgetCallback && shouldRenderWidgets(client)) {
// We got a custom renderer!
const lineStrings = text.split("\n");
const lines: { from: number; to: number }[] = [];
let fromIt = from;
for (const line of lineStrings) {
lines.push({
from: fromIt,
to: fromIt + line.length,
});
fromIt += line.length + 1;
}
const firstLine = lines[0],
lastLine = lines[lines.length - 1];
// In case of doubt, back out
if (!firstLine || !lastLine) return;
widgets.push(
invisibleDecoration.range(firstLine.from, firstLine.to),
);
widgets.push(invisibleDecoration.range(lastLine.from, lastLine.to));
widgets.push(
Decoration.line({
class: "sb-fenced-code-iframe",
}).range(firstLine.from),
);
widgets.push(
Decoration.line({
class: "sb-fenced-code-hide",
}).range(lastLine.from),
);
lines.slice(1, lines.length - 1).forEach((line) => {
widgets.push(
Decoration.line({ class: "sb-line-table-outside" }).range(
line.from,
),
);
});
const widget = new IFrameWidget(
client,
lineStrings.slice(1, lineStrings.length - 1).join("\n"),
codeWidgetCallback,
);
widgets.push(
Decoration.widget({
widget: widget,
}).range(from),
);
return false;
}
return true;
}
if (name === "CodeMark") {
const parent = node.parent!;
// Hide ONLY if CodeMark is not insine backticks (InlineCode) and the cursor is placed outside
if (
parent.node.name !== "InlineCode" &&
!isCursorInRange(state, [parent.from, parent.to])
) {
widgets.push(
Decoration.line({
class: "sb-line-code-outside",
}).range(state.doc.lineAt(from).from),
);
}
}
},
});
return Decoration.set(widgets, true);
});
}