2022-12-12 16:16:14 +08:00
|
|
|
import {
|
|
|
|
Decoration,
|
|
|
|
EditorState,
|
|
|
|
EditorView,
|
|
|
|
SyntaxNodeRef,
|
|
|
|
syntaxTree,
|
|
|
|
WidgetType,
|
|
|
|
} from "../deps.ts";
|
2023-07-14 22:56:20 +08:00
|
|
|
import { Client } from "../client.ts";
|
2022-12-12 16:16:14 +08:00
|
|
|
import { decoratorStateField, isCursorInRange } from "./util.ts";
|
|
|
|
|
2023-01-23 01:53:14 +08:00
|
|
|
const ADMONITION_REGEX =
|
2024-03-05 01:35:02 +08:00
|
|
|
/^>( *)(?:\*{2}|\[!)(.*?)(\*{2}|\])( *)(.*)(?:\n([\s\S]*))?/im;
|
2022-12-12 16:16:14 +08:00
|
|
|
const ADMONITION_LINE_SPLIT_REGEX = /\n>/gm;
|
|
|
|
|
|
|
|
class AdmonitionIconWidget extends WidgetType {
|
|
|
|
constructor(
|
|
|
|
readonly pos: number,
|
2024-03-05 01:35:02 +08:00
|
|
|
readonly type: string,
|
2022-12-12 16:16:14 +08:00
|
|
|
readonly editorView: EditorView,
|
|
|
|
) {
|
|
|
|
super();
|
|
|
|
}
|
|
|
|
|
|
|
|
toDOM(): HTMLElement {
|
|
|
|
const outerDiv = document.createElement("div");
|
|
|
|
outerDiv.classList.add("sb-admonition-icon");
|
2023-11-21 23:24:20 +08:00
|
|
|
outerDiv.addEventListener("click", () => {
|
2022-12-12 16:16:14 +08:00
|
|
|
this.editorView.dispatch({
|
|
|
|
selection: {
|
|
|
|
anchor: this.pos,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
return outerDiv;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type AdmonitionFields = {
|
|
|
|
preSpaces: string;
|
2024-03-05 01:35:02 +08:00
|
|
|
admonitionType: string;
|
2024-03-08 01:52:04 +08:00
|
|
|
postSyntax: string;
|
2022-12-12 16:16:14 +08:00
|
|
|
postSpaces: string;
|
|
|
|
admonitionTitle: string;
|
|
|
|
admonitionContent: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Given the raw text of an entire Blockquote, match an Admonition block.
|
|
|
|
// If matched, extract relevant fields using regex capture groups and return them
|
|
|
|
// as an object.
|
|
|
|
//
|
|
|
|
// If not matched return null.
|
|
|
|
//
|
|
|
|
// Example Admonition block (github formatted):
|
|
|
|
//
|
|
|
|
// > **note** I am an Admonition Title
|
|
|
|
// > admonition text
|
|
|
|
//
|
2024-03-05 01:35:02 +08:00
|
|
|
// or
|
|
|
|
// > [!note] I am an Admonition Title
|
|
|
|
// > admonition text
|
|
|
|
|
2022-12-12 16:16:14 +08:00
|
|
|
function extractAdmonitionFields(rawText: string): AdmonitionFields | null {
|
|
|
|
const regexResults = rawText.match(ADMONITION_REGEX);
|
|
|
|
|
|
|
|
if (regexResults) {
|
2023-01-23 01:53:14 +08:00
|
|
|
const preSpaces = regexResults[1] || "";
|
2024-03-05 01:35:02 +08:00
|
|
|
const admonitionType = regexResults[2];
|
|
|
|
const postSyntax = regexResults[3];
|
|
|
|
const postSpaces = regexResults[4] || "";
|
|
|
|
const admonitionTitle: string = regexResults[5] || "";
|
|
|
|
const admonitionContent: string = regexResults[6] || "";
|
2023-01-23 01:53:14 +08:00
|
|
|
|
|
|
|
return {
|
|
|
|
preSpaces,
|
|
|
|
admonitionType,
|
2024-03-05 01:35:02 +08:00
|
|
|
postSyntax,
|
2023-01-23 01:53:14 +08:00
|
|
|
postSpaces,
|
|
|
|
admonitionTitle,
|
|
|
|
admonitionContent,
|
|
|
|
};
|
2022-12-12 16:16:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-07-14 22:56:20 +08:00
|
|
|
export function admonitionPlugin(editor: Client) {
|
2022-12-12 16:16:14 +08:00
|
|
|
return decoratorStateField((state: EditorState) => {
|
|
|
|
const widgets: any[] = [];
|
|
|
|
|
2024-03-08 01:52:04 +08:00
|
|
|
// Get admonition styles from stylesheets
|
|
|
|
const allStyles = [...document.styleSheets]
|
|
|
|
.map((styleSheet) => {
|
|
|
|
return [...styleSheet.cssRules].map((rule) => rule.cssText).join("");
|
|
|
|
}).filter(Boolean).join("\n");
|
|
|
|
const admonitionStyles = allStyles.match(/(?<=admonition=").*?(?=")/g);
|
|
|
|
|
2022-12-12 16:16:14 +08:00
|
|
|
syntaxTree(state).iterate({
|
|
|
|
enter: (node: SyntaxNodeRef) => {
|
|
|
|
const { type, from, to } = node;
|
|
|
|
|
|
|
|
if (type.name === "Blockquote") {
|
|
|
|
// Extract raw text from admonition block
|
|
|
|
const rawText = state.sliceDoc(from, to);
|
|
|
|
|
|
|
|
// Split text into type, title and content using regex capture groups
|
|
|
|
const extractedFields = extractAdmonitionFields(rawText);
|
|
|
|
|
|
|
|
// Bailout here if we don't have a proper Admonition formatted blockquote
|
|
|
|
if (!extractedFields) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-03-08 01:52:04 +08:00
|
|
|
const { preSpaces, admonitionType, postSyntax, postSpaces } =
|
|
|
|
extractedFields;
|
|
|
|
|
|
|
|
if (!admonitionStyles?.includes(admonitionType)) {
|
|
|
|
return;
|
|
|
|
}
|
2022-12-12 16:16:14 +08:00
|
|
|
|
|
|
|
// A blockquote is actually rendered as many divs, one per line.
|
|
|
|
// We need to keep track of the `from` offsets here, so we can attach css
|
|
|
|
// classes to them further down.
|
|
|
|
const fromOffsets: number[] = [];
|
|
|
|
const lines = rawText.slice(1).split(ADMONITION_LINE_SPLIT_REGEX);
|
|
|
|
let accum = from;
|
2023-01-23 01:53:14 +08:00
|
|
|
lines.forEach((line) => {
|
2022-12-12 16:16:14 +08:00
|
|
|
fromOffsets.push(accum);
|
|
|
|
accum += line.length + 2;
|
|
|
|
});
|
|
|
|
|
2024-03-05 01:35:02 +08:00
|
|
|
// `from` and `to` range info for switching out keyword text with correct
|
2022-12-12 16:16:14 +08:00
|
|
|
// icon further down.
|
|
|
|
const iconRange = {
|
|
|
|
from: from + 1,
|
2024-03-08 01:52:04 +08:00
|
|
|
to: from + preSpaces.length + 2 + admonitionType.length +
|
|
|
|
postSyntax.length +
|
2023-01-23 01:53:14 +08:00
|
|
|
postSpaces.length + 1,
|
2022-12-12 16:16:14 +08:00
|
|
|
};
|
|
|
|
|
2024-03-05 01:35:02 +08:00
|
|
|
// The first div is the title, attach title css class
|
2022-12-12 16:16:14 +08:00
|
|
|
widgets.push(
|
2023-01-23 01:53:14 +08:00
|
|
|
Decoration.line({
|
2024-03-05 01:35:02 +08:00
|
|
|
class: "sb-admonition-title",
|
2023-01-23 01:53:14 +08:00
|
|
|
}).range(fromOffsets[0]),
|
2022-12-12 16:16:14 +08:00
|
|
|
);
|
|
|
|
|
2024-03-05 01:35:02 +08:00
|
|
|
// If cursor is not within the first line, replace the keyword text
|
|
|
|
// with the icon
|
2023-01-23 01:53:14 +08:00
|
|
|
if (
|
|
|
|
!isCursorInRange(state, [
|
|
|
|
from,
|
|
|
|
fromOffsets.length > 1 ? fromOffsets[1] : to,
|
|
|
|
])
|
|
|
|
) {
|
2022-12-12 16:16:14 +08:00
|
|
|
widgets.push(
|
|
|
|
Decoration.replace({
|
|
|
|
widget: new AdmonitionIconWidget(
|
|
|
|
iconRange.from + 1,
|
|
|
|
admonitionType,
|
2023-07-27 17:41:44 +08:00
|
|
|
editor.editorView,
|
2022-12-12 16:16:14 +08:00
|
|
|
),
|
|
|
|
inclusive: true,
|
2023-01-23 01:53:14 +08:00
|
|
|
}).range(iconRange.from, iconRange.to),
|
2022-12-12 16:16:14 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Each line of the blockquote is spread across separate divs, attach
|
2024-03-05 01:35:02 +08:00
|
|
|
// relevant css classes and attribute here.
|
|
|
|
fromOffsets.forEach((fromOffset) => {
|
2022-12-12 16:16:14 +08:00
|
|
|
widgets.push(
|
2024-03-05 01:35:02 +08:00
|
|
|
Decoration.line({
|
|
|
|
attributes: { admonition: admonitionType },
|
|
|
|
class: "sb-admonition",
|
|
|
|
}).range(fromOffset),
|
2022-12-12 16:16:14 +08:00
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
return Decoration.set(widgets, true);
|
|
|
|
});
|
|
|
|
}
|