+30
-68
@@ -1,76 +1,38 @@
|
||||
import { Decoration, EditorState, foldedRanges, syntaxTree } from "../deps.ts";
|
||||
import { Decoration, EditorState, syntaxTree } from "../deps.ts";
|
||||
import {
|
||||
decoratorStateField,
|
||||
HtmlWidget,
|
||||
invisibleDecoration,
|
||||
isCursorInRange,
|
||||
} from "./util.ts";
|
||||
|
||||
function hideNodes(state: EditorState) {
|
||||
const widgets: any[] = [];
|
||||
const foldRanges = foldedRanges(state);
|
||||
syntaxTree(state).iterate({
|
||||
enter(node) {
|
||||
if (
|
||||
node.name === "HorizontalRule" &&
|
||||
!isCursorInRange(state, [node.from, node.to])
|
||||
) {
|
||||
widgets.push(invisibleDecoration.range(node.from, node.to));
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-line-hr",
|
||||
}).range(node.from),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
node.name === "Image" &&
|
||||
!isCursorInRange(state, [node.from, node.to])
|
||||
) {
|
||||
widgets.push(invisibleDecoration.range(node.from, node.to));
|
||||
}
|
||||
|
||||
if (
|
||||
node.name === "FrontMatterMarker"
|
||||
) {
|
||||
const parent = node.node.parent!;
|
||||
|
||||
const folded = foldRanges.iter();
|
||||
let shouldShowFrontmatterBanner = false;
|
||||
while (folded.value) {
|
||||
// Check if cursor is in the folded range
|
||||
if (isCursorInRange(state, [folded.from, folded.to])) {
|
||||
// console.log("Cursor is in folded area, ");
|
||||
shouldShowFrontmatterBanner = true;
|
||||
break;
|
||||
}
|
||||
folded.next();
|
||||
}
|
||||
if (!isCursorInRange(state, [parent.from, parent.to])) {
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-line-frontmatter-outside",
|
||||
}).range(node.from),
|
||||
);
|
||||
shouldShowFrontmatterBanner = true;
|
||||
}
|
||||
if (shouldShowFrontmatterBanner && parent.from === node.from) {
|
||||
// Only put this on the first line of the frontmatter
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new HtmlWidget(
|
||||
`frontmatter`,
|
||||
"sb-frontmatter-marker",
|
||||
),
|
||||
}).range(node.from),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
}
|
||||
|
||||
export function cleanBlockPlugin() {
|
||||
return decoratorStateField(hideNodes);
|
||||
return decoratorStateField(
|
||||
(state: EditorState) => {
|
||||
const widgets: any[] = [];
|
||||
|
||||
syntaxTree(state).iterate({
|
||||
enter(node) {
|
||||
if (
|
||||
node.name === "HorizontalRule" &&
|
||||
!isCursorInRange(state, [node.from, node.to])
|
||||
) {
|
||||
widgets.push(invisibleDecoration.range(node.from, node.to));
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-line-hr",
|
||||
}).range(node.from),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
node.name === "Image" &&
|
||||
!isCursorInRange(state, [node.from, node.to])
|
||||
) {
|
||||
widgets.push(invisibleDecoration.range(node.from, node.to));
|
||||
}
|
||||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+11
-9
@@ -12,33 +12,35 @@ import { taskListPlugin } from "./task.ts";
|
||||
import { cleanWikiLinkPlugin } from "./wiki_link.ts";
|
||||
import { cleanCommandLinkPlugin } from "./command_link.ts";
|
||||
import { fencedCodePlugin } from "./fenced_code.ts";
|
||||
import { frontmatterPlugin } from "./frontmatter.ts";
|
||||
|
||||
export function cleanModePlugins(editor: Client) {
|
||||
export function cleanModePlugins(client: Client) {
|
||||
return [
|
||||
linkPlugin(editor),
|
||||
linkPlugin(client),
|
||||
blockquotePlugin(),
|
||||
admonitionPlugin(editor),
|
||||
admonitionPlugin(client),
|
||||
hideMarksPlugin(),
|
||||
hideHeaderMarkPlugin(),
|
||||
cleanBlockPlugin(),
|
||||
fencedCodePlugin(editor),
|
||||
frontmatterPlugin(client),
|
||||
fencedCodePlugin(client),
|
||||
taskListPlugin({
|
||||
// TODO: Move this logic elsewhere?
|
||||
onCheckboxClick: (pos) => {
|
||||
const clickEvent: ClickEvent = {
|
||||
page: editor.currentPage!,
|
||||
page: client.currentPage!,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
pos: pos,
|
||||
};
|
||||
// Propagate click event from checkbox
|
||||
editor.dispatchAppEvent("page:click", clickEvent);
|
||||
client.dispatchAppEvent("page:click", clickEvent);
|
||||
},
|
||||
}),
|
||||
listBulletPlugin(),
|
||||
tablePlugin(editor),
|
||||
cleanWikiLinkPlugin(editor),
|
||||
cleanCommandLinkPlugin(editor),
|
||||
tablePlugin(client),
|
||||
cleanWikiLinkPlugin(client),
|
||||
cleanCommandLinkPlugin(client),
|
||||
] as Extension[];
|
||||
}
|
||||
|
||||
@@ -71,11 +71,15 @@ export function fencedCodePlugin(editor: Client) {
|
||||
);
|
||||
});
|
||||
|
||||
const bodyText = lineStrings.slice(1, lineStrings.length - 1).join(
|
||||
"\n",
|
||||
);
|
||||
const widget = renderMode === "markdown"
|
||||
? new MarkdownWidget(
|
||||
from + lineStrings[0].length + 1,
|
||||
editor,
|
||||
lineStrings.slice(1, lineStrings.length - 1).join("\n"),
|
||||
`widget:${editor.currentPage}:${bodyText}`,
|
||||
bodyText,
|
||||
codeWidgetCallback,
|
||||
"sb-markdown-widget",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Client } from "../client.ts";
|
||||
import { Decoration, EditorState, syntaxTree } from "../deps.ts";
|
||||
import { MarkdownWidget } from "./markdown_widget.ts";
|
||||
import { decoratorStateField, HtmlWidget, isCursorInRange } from "./util.ts";
|
||||
|
||||
export function frontmatterPlugin(client: Client) {
|
||||
const panelWidgetHook = client.system.panelWidgetHook;
|
||||
const frontmatterCallback = panelWidgetHook.callbacks.get("frontmatter");
|
||||
return decoratorStateField(
|
||||
(state: EditorState) => {
|
||||
const widgets: any[] = [];
|
||||
|
||||
syntaxTree(state).iterate({
|
||||
enter(node) {
|
||||
if (
|
||||
node.name === "FrontMatter"
|
||||
) {
|
||||
if (!isCursorInRange(state, [node.from, node.to])) {
|
||||
if (frontmatterCallback) {
|
||||
// Render as a widget
|
||||
const text = state.sliceDoc(node.from, node.to);
|
||||
const lineStrings = text.split("\n");
|
||||
|
||||
const lines: { from: number; to: number }[] = [];
|
||||
let fromIt = node.from;
|
||||
for (const line of lineStrings) {
|
||||
lines.push({
|
||||
from: fromIt,
|
||||
to: fromIt + line.length,
|
||||
});
|
||||
fromIt += line.length + 1;
|
||||
}
|
||||
|
||||
lines.slice(0, lines.length - 1).forEach((line) => {
|
||||
widgets.push(
|
||||
// Reusing line-table-outside here for laziness reasons
|
||||
Decoration.line({ class: "sb-line-table-outside" }).range(
|
||||
line.from,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new MarkdownWidget(
|
||||
undefined,
|
||||
client,
|
||||
`frontmatter:${client.currentPage}`,
|
||||
"",
|
||||
frontmatterCallback,
|
||||
"sb-markdown-frontmatter-widget",
|
||||
),
|
||||
block: true,
|
||||
}).range(lines[lines.length - 1].from),
|
||||
);
|
||||
} else if (!frontmatterCallback) {
|
||||
// Not rendering as a widget
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new HtmlWidget(
|
||||
`frontmatter`,
|
||||
"sb-frontmatter-marker",
|
||||
),
|
||||
}).range(node.from),
|
||||
);
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-line-frontmatter-outside",
|
||||
}).range(node.from),
|
||||
);
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-line-frontmatter-outside",
|
||||
}).range(state.doc.lineAt(node.to).from),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -5,17 +5,16 @@ import { renderMarkdownToHtml } from "../../plugs/markdown/markdown_render.ts";
|
||||
import { resolveAttachmentPath } from "$sb/lib/resolve.ts";
|
||||
import { parse } from "../../common/markdown_parser/parse_tree.ts";
|
||||
import buildMarkdown from "../../common/markdown_parser/parser.ts";
|
||||
import { renderToText } from "$sb/lib/tree.ts";
|
||||
|
||||
const activeWidgets = new Set<MarkdownWidget>();
|
||||
|
||||
export class MarkdownWidget extends WidgetType {
|
||||
renderedMarkdown?: string;
|
||||
public dom?: HTMLElement;
|
||||
|
||||
constructor(
|
||||
readonly from: number | undefined,
|
||||
readonly client: Client,
|
||||
readonly cacheKey: string,
|
||||
readonly bodyText: string,
|
||||
readonly codeWidgetCallback: CodeWidgetCallback,
|
||||
readonly className: string,
|
||||
@@ -26,11 +25,12 @@ export class MarkdownWidget extends WidgetType {
|
||||
toDOM(): HTMLElement {
|
||||
const div = document.createElement("div");
|
||||
div.className = this.className;
|
||||
const cacheItem = this.client.getWidgetCache(this.bodyText);
|
||||
const cacheItem = this.client.getWidgetCache(this.cacheKey);
|
||||
if (cacheItem) {
|
||||
div.innerHTML = this.wrapHtml(
|
||||
cacheItem.html,
|
||||
cacheItem.buttons,
|
||||
cacheItem.buttons || [],
|
||||
cacheItem.banner,
|
||||
);
|
||||
this.attachListeners(div, cacheItem.buttons);
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export class MarkdownWidget extends WidgetType {
|
||||
if (!widgetContent) {
|
||||
div.innerHTML = "";
|
||||
this.client.setWidgetCache(
|
||||
this.bodyText,
|
||||
this.cacheKey,
|
||||
{ height: div.clientHeight, html: "" },
|
||||
);
|
||||
return;
|
||||
@@ -73,8 +73,6 @@ export class MarkdownWidget extends WidgetType {
|
||||
this.client.currentPage,
|
||||
],
|
||||
);
|
||||
// Used for the source button
|
||||
this.renderedMarkdown = renderToText(mdTree);
|
||||
|
||||
const html = renderMarkdownToHtml(mdTree, {
|
||||
// Annotate every element with its position so we can use it to put
|
||||
@@ -97,27 +95,48 @@ export class MarkdownWidget extends WidgetType {
|
||||
// HTML still same as in cache, no need to re-render
|
||||
return;
|
||||
}
|
||||
div.innerHTML = this.wrapHtml(html, widgetContent.buttons);
|
||||
div.innerHTML = this.wrapHtml(
|
||||
html,
|
||||
widgetContent.buttons || [],
|
||||
widgetContent.banner,
|
||||
);
|
||||
this.attachListeners(div, widgetContent.buttons);
|
||||
|
||||
// Let's give it a tick, then measure and cache
|
||||
setTimeout(() => {
|
||||
this.client.setWidgetCache(
|
||||
this.bodyText,
|
||||
{ height: div.offsetHeight, html, buttons: widgetContent.buttons },
|
||||
this.cacheKey,
|
||||
{
|
||||
height: div.offsetHeight,
|
||||
html,
|
||||
buttons: widgetContent.buttons,
|
||||
banner: widgetContent.banner,
|
||||
},
|
||||
);
|
||||
// Because of the rejiggering of the DOM, we need to do a no-op cursor move to make sure it's positioned correctly
|
||||
this.client.editorView.dispatch({
|
||||
selection: {
|
||||
anchor: this.client.editorView.state.selection.main.anchor,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private wrapHtml(html: string, buttons?: CodeWidgetButton[]) {
|
||||
if (!buttons) {
|
||||
return html;
|
||||
private wrapHtml(
|
||||
html: string,
|
||||
buttons: CodeWidgetButton[],
|
||||
banner?: string,
|
||||
) {
|
||||
if (!html) {
|
||||
return "";
|
||||
}
|
||||
return `<div class="button-bar">${
|
||||
buttons.map((button, idx) =>
|
||||
buttons.filter((button) => !button.widgetTarget).map((button, idx) =>
|
||||
`<button data-button="${idx}" title="${button.description}">${button.svg}</button> `
|
||||
).join("")
|
||||
}</div>${html}`;
|
||||
}</div>${
|
||||
banner ? `<div class="sb-banner">${escapeHtml(banner)}</div>` : ""
|
||||
}${html}`;
|
||||
}
|
||||
|
||||
private attachListeners(div: HTMLElement, buttons?: CodeWidgetButton[]) {
|
||||
@@ -126,6 +145,7 @@ export class MarkdownWidget extends WidgetType {
|
||||
// Override default click behavior with a local navigate (faster)
|
||||
el.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const [pageName, pos] = el.dataset.ref!.split(/[$@]/);
|
||||
if (pos && pos.match(/^\d+$/)) {
|
||||
this.client.navigate(pageName, +pos);
|
||||
@@ -138,9 +158,18 @@ export class MarkdownWidget extends WidgetType {
|
||||
// Implement task toggling
|
||||
div.querySelectorAll("span[data-external-task-ref]").forEach((el: any) => {
|
||||
const taskRef = el.dataset.externalTaskRef;
|
||||
el.querySelector("input[type=checkbox]").addEventListener(
|
||||
const input = el.querySelector("input[type=checkbox]")!;
|
||||
input.addEventListener(
|
||||
"click",
|
||||
(e: any) => {
|
||||
// Avoid triggering the click on the parent
|
||||
e.stopPropagation();
|
||||
},
|
||||
);
|
||||
input.addEventListener(
|
||||
"change",
|
||||
(e: any) => {
|
||||
e.stopPropagation();
|
||||
const oldState = e.target.dataset.state;
|
||||
const newState = oldState === " " ? "x" : " ";
|
||||
// Update state in DOM as well for future toggles
|
||||
@@ -162,29 +191,37 @@ export class MarkdownWidget extends WidgetType {
|
||||
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
const button = buttons[i];
|
||||
div.querySelector(`button[data-button="${i}"]`)!.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
console.log("Button clicked:", button.description);
|
||||
if (button.widgetTarget) {
|
||||
div.addEventListener("click", () => {
|
||||
console.log("Widget clicked");
|
||||
this.client.system.localSyscall("system.invokeFunction", [
|
||||
button.invokeFunction,
|
||||
this.from,
|
||||
]).then((newContent: string | undefined) => {
|
||||
if (newContent) {
|
||||
div.innerText = newContent;
|
||||
}
|
||||
this.client.focus();
|
||||
}).catch(console.error);
|
||||
},
|
||||
);
|
||||
]).catch(console.error);
|
||||
});
|
||||
} else {
|
||||
div.querySelector(`button[data-button="${i}"]`)!.addEventListener(
|
||||
"click",
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
console.log("Button clicked:", button.description);
|
||||
this.client.system.localSyscall("system.invokeFunction", [
|
||||
button.invokeFunction,
|
||||
this.from,
|
||||
]).then((newContent: string | undefined) => {
|
||||
if (newContent) {
|
||||
div.innerText = newContent;
|
||||
}
|
||||
this.client.focus();
|
||||
}).catch(console.error);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
// div.querySelectorAll("ul > li").forEach((el) => {
|
||||
// el.classList.add("sb-line-li-1", "sb-line-ul");
|
||||
// });
|
||||
}
|
||||
|
||||
get estimatedHeight(): number {
|
||||
const cacheItem = this.client.getWidgetCache(this.bodyText);
|
||||
const cacheItem = this.client.getWidgetCache(this.cacheKey);
|
||||
// console.log("Calling estimated height", this.bodyText, cacheItem);
|
||||
return cacheItem ? cacheItem.height : -1;
|
||||
}
|
||||
@@ -192,7 +229,7 @@ export class MarkdownWidget extends WidgetType {
|
||||
eq(other: WidgetType): boolean {
|
||||
return (
|
||||
other instanceof MarkdownWidget &&
|
||||
other.bodyText === this.bodyText
|
||||
other.bodyText === this.bodyText && other.cacheKey === this.cacheKey
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -218,3 +255,10 @@ function garbageCollectWidgets() {
|
||||
}
|
||||
|
||||
setInterval(garbageCollectWidgets, 5000);
|
||||
|
||||
function escapeHtml(text: string) {
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(
|
||||
/>/g,
|
||||
">",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ const straightQuoteContexts = [
|
||||
"FrontMatterCode",
|
||||
"Attribute",
|
||||
"CommandLink",
|
||||
"TemplateDirective",
|
||||
];
|
||||
|
||||
// TODO: Add support for selection (put quotes around or create blockquote block?)
|
||||
|
||||
@@ -17,6 +17,7 @@ export function postScriptPrefacePlugin(
|
||||
undefined,
|
||||
editor,
|
||||
`top:${editor.currentPage}`,
|
||||
"",
|
||||
topCallback,
|
||||
"sb-markdown-top-widget",
|
||||
),
|
||||
@@ -33,6 +34,7 @@ export function postScriptPrefacePlugin(
|
||||
undefined,
|
||||
editor,
|
||||
`bottom:${editor.currentPage}`,
|
||||
"",
|
||||
bottomCallback,
|
||||
"sb-markdown-bottom-widget",
|
||||
),
|
||||
|
||||
@@ -166,8 +166,9 @@ export function shouldRenderAsCode(
|
||||
if (mainSelection.empty) {
|
||||
return checkRangeOverlap(range, [mainSelection.from, mainSelection.to]);
|
||||
} else {
|
||||
// If the selection is encompassing the fenced code we render as code
|
||||
return checkRangeSubset([mainSelection.from, mainSelection.to], range);
|
||||
// If the selection is encompassing the fenced code we render as code, or vice versa
|
||||
return checkRangeSubset([mainSelection.from, mainSelection.to], range) ||
|
||||
checkRangeSubset(range, [mainSelection.from, mainSelection.to]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user