Files

190 lines
6.3 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,
widgetRenderMode,
} from "./util.ts";
import { IFrameWidget } from "./iframe_widget.ts";
import { LoadingWidget } from "./loading_widget.ts";
import { LuaWidget } from "./lua_widget.ts";
import { renderLuaCallback } from "../space_lua/render_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(/^(?:```+|~~~+)(\S+)?/)!;
const renderMode = widgetRenderMode(client);
const luaWidgetDef = lang
? client.clientSystem.luaCodeWidgets.get(lang)
: undefined;
const codeWidgetCallback =
client.clientSystem.codeWidgetHook.codeWidgetCallbacks.get(lang);
// Lua-registered code widgets win precedence over plug ones.
if (luaWidgetDef && renderMode !== "disabled") {
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 bodyText = lineStrings
.slice(1, lineStrings.length - 1)
.join("\n");
const pageName = client.currentName();
const currentPageMeta = client.currentPageMeta();
const widget =
renderMode === "loading"
? new LoadingWidget(true)
: new LuaWidget({
client,
cacheKey: `codewidget:${lang}:${pageName}:${bodyText}`,
expressionText: bodyText,
codeText: text,
callback: () =>
renderLuaCallback(
client,
luaWidgetDef.render,
[bodyText, pageName],
currentPageMeta,
),
renderEmpty: false,
inPage: true,
});
widgets.push(
Decoration.widget({
widget: widget,
}).range(from),
);
return false;
}
// Only custom render when we have a custom renderer, and the current page is not a template
if (codeWidgetCallback && renderMode !== "disabled") {
// 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 =
renderMode === "loading"
? new LoadingWidget(true)
: 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);
});
}