pull/229/head
Zef Hemel 2022-12-22 16:20:05 +01:00
parent fbb45f0525
commit cca48f66cd
10 changed files with 306 additions and 34 deletions

View File

@ -5,6 +5,7 @@ import { EventHookT } from "../plugos/hooks/event.ts";
import { CommandHookT } from "../web/hooks/command.ts";
import { SlashCommandHookT } from "../web/hooks/slash_command.ts";
import { PageNamespaceHookT } from "../server/hooks/page_namespace.ts";
import { CodeWidgetT } from "../web/hooks/code_widget.ts";
export type SilverBulletHooks =
& CommandHookT
@ -12,6 +13,7 @@ export type SilverBulletHooks =
& EndpointHookT
& CronHookT
& EventHookT
& CodeWidgetT
& PageNamespaceHookT;
export type SyntaxExtensions = {

View File

@ -30,4 +30,8 @@ functions:
sharePublisher:
path: ./share.ts:sharePublisher
events:
- share:file
- share:file
markdownWidget:
path: ./widget.ts:markdownWidget
codeWidget: markdown

19
plugs/markdown/widget.ts Normal file
View File

@ -0,0 +1,19 @@
import { parseMarkdown } from "$sb/silverbullet-syscall/markdown.ts";
import { renderMarkdownToHtml } from "./markdown_render.ts";
export async function markdownWidget(
bodyText: string,
): Promise<{ html: string; script: string }> {
const mdTree = await parseMarkdown(bodyText);
const html = renderMarkdownToHtml(mdTree, {
smartHardBreak: true,
});
return Promise.resolve({
html: html,
script: `updateHeight();
document.addEventListener("click", () => {
api({type: "blur"});
});`,
});
}

View File

@ -52,23 +52,6 @@ function hideNodes(state: EditorState) {
}
}
}
if (
node.name === "CodeMark"
) {
const parent = node.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(node.from),
);
}
}
},
});
return Decoration.set(widgets, true);

View File

@ -12,6 +12,7 @@ import { tablePlugin } from "./table.ts";
import { taskListPlugin } from "./task.ts";
import { cleanWikiLinkPlugin } from "./wiki_link.ts";
import { cleanCommandLinkPlugin } from "./command_link.ts";
import { fencedCodePlugin } from "./fenced_code.ts";
export function cleanModePlugins(editor: Editor) {
return [
@ -22,6 +23,7 @@ export function cleanModePlugins(editor: Editor) {
hideMarksPlugin(),
hideHeaderMarkPlugin(),
cleanBlockPlugin(),
fencedCodePlugin(editor),
taskListPlugin({
// TODO: Move this logic elsewhere?
onCheckboxClick: (pos) => {

View File

@ -0,0 +1,176 @@
import { panelHtml } from "../components/panel.tsx";
import { Decoration, EditorState, syntaxTree, WidgetType } from "../deps.ts";
import type { Editor } from "../editor.tsx";
import { CodeWidgetCallback } from "../hooks/code_widget.ts";
import {
decoratorStateField,
invisibleDecoration,
isCursorInRange,
} from "./util.ts";
class IFrameWidget extends WidgetType {
constructor(
readonly from: number,
readonly to: number,
readonly editor: Editor,
readonly bodyText: string,
readonly codeWidgetCallback: CodeWidgetCallback,
) {
super();
}
toDOM(): HTMLElement {
const iframe = document.createElement("iframe");
iframe.srcdoc = panelHtml;
// iframe.style.height = "0";
const messageListener = (evt: any) => {
if (evt.source !== iframe.contentWindow) {
return;
}
const data = evt.data;
if (!data) {
return;
}
switch (data.type) {
case "event":
this.editor.dispatchAppEvent(data.name, ...data.args);
break;
case "setHeight":
iframe.style.height = data.height + "px";
break;
case "setBody":
this.editor.editorView!.dispatch({
changes: {
from: this.from,
to: this.to,
insert: data.body,
},
});
break;
case "blur":
this.editor.editorView!.dispatch({
selection: { anchor: this.from },
});
this.editor.focus();
break;
}
};
iframe.onload = () => {
// Subscribe to message event on global object (to receive messages from iframe)
globalThis.addEventListener("message", messageListener);
this.codeWidgetCallback(this.bodyText).then(({ html, script }) => {
iframe.contentWindow!.postMessage({
type: "html",
html,
script,
});
iframe.contentWindow!.onunload = () => {
// Unsubscribing from events
globalThis.removeEventListener("message", messageListener);
};
});
};
return iframe;
}
eq(other: WidgetType): boolean {
return (
other instanceof IFrameWidget &&
other.bodyText === this.bodyText
);
}
}
export function fencedCodePlugin(editor: Editor) {
return decoratorStateField((state: EditorState) => {
const widgets: any[] = [];
syntaxTree(state).iterate({
enter({ from, to, name, node }) {
if (name === "FencedCode") {
if (isCursorInRange(state, [from, to])) return;
const text = state.sliceDoc(from, to);
const [_, lang] = text.match(/^```(\w+)?/)!;
const codeWidgetCallback = editor.codeWidgetHook.codeWidgetCallbacks
.get(lang);
if (codeWidgetCallback) {
// 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,
),
);
});
widgets.push(
Decoration.widget({
widget: new IFrameWidget(
from + lineStrings[0].length + 1,
to - lineStrings[lineStrings.length - 1].length - 1,
editor,
lineStrings.slice(1, lineStrings.length - 1).join("\n"),
codeWidgetCallback,
),
}).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(node.from),
);
}
}
},
});
return Decoration.set(widgets, true);
});
}

View File

@ -2,7 +2,7 @@ import { useEffect, useRef } from "../deps.ts";
import { Editor } from "../editor.tsx";
import { PanelConfig } from "../types.ts";
const panelHtml = `<!DOCTYPE html>
export const panelHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
@ -25,14 +25,27 @@ window.addEventListener("message", (message) => {
});
function sendEvent(name, ...args) {
window.parent.postMessage(
{
type: "event",
name,
args,
},
"*"
);
window.parent.postMessage({ type: "event", name, args, }, "*");
}
function api(obj) {
window.parent.postMessage(obj, "*");
}
function updateHeight() {
api({
type: "setHeight",
height: document.documentElement.offsetHeight,
});
}
function loadJsByUrl(url) {
const script = document.createElement("script");
script.src = url;
return new Promise((resolve) => {
script.onload = resolve;
document.documentElement.firstChild.appendChild(script);
});
}
</script>
</head>

View File

@ -89,19 +89,13 @@ import { storeSyscalls } from "./syscalls/store.ts";
import { systemSyscalls } from "./syscalls/system.ts";
import { AppViewState, BuiltinSettings, initialViewState } from "./types.ts";
// Third-party dependencies
// PlugOS Dependencies
// Syscalls
// State and state transitions
import type {
AppEvent,
ClickEvent,
CompleteEvent,
} from "../plug-api/app_event.ts";
import { CodeWidgetHook } from "./hooks/code_widget.ts";
// UI Components
// CodeMirror plugins
// Real-time collaboration
const frontMatterRegex = /^---\n(.*?)---\n/ms;
class PageState {
@ -124,6 +118,8 @@ export class Editor {
space: Space;
pageNavigator: PathPageNavigator;
eventHook: EventHook;
codeWidgetHook: CodeWidgetHook;
saveTimeout: any;
debouncedUpdateEvent = throttle(() => {
this.eventHook
@ -157,6 +153,10 @@ export class Editor {
this.eventHook = new EventHook();
this.system.addHook(this.eventHook);
// Code widget hook
this.codeWidgetHook = new CodeWidgetHook();
this.system.addHook(this.codeWidgetHook);
// Command hook
this.commandHook = new CommandHook();
this.commandHook.on({

57
web/hooks/code_widget.ts Normal file
View File

@ -0,0 +1,57 @@
import { Hook, Manifest } from "../../plugos/types.ts";
import { System } from "../../plugos/system.ts";
export type CodeWidgetT = {
codeWidget?: string;
};
export type CodeWidgetCallback = (
bodyText: string,
) => Promise<{ html: string; script: string }>;
export class CodeWidgetHook implements Hook<CodeWidgetT> {
codeWidgetCallbacks = new Map<string, CodeWidgetCallback>();
constructor() {
}
collectAllCodeWidgets(system: System<CodeWidgetT>) {
this.codeWidgetCallbacks.clear();
for (const plug of system.loadedPlugs.values()) {
for (
const [name, functionDef] of Object.entries(
plug.manifest!.functions,
)
) {
if (!functionDef.codeWidget) {
continue;
}
this.codeWidgetCallbacks.set(functionDef.codeWidget, (bodyText) => {
return plug.invoke(name, [bodyText]);
});
}
}
}
apply(system: System<CodeWidgetT>): void {
this.collectAllCodeWidgets(system);
system.on({
plugLoaded: () => {
this.collectAllCodeWidgets(system);
},
});
}
validateManifest(manifest: Manifest<CodeWidgetT>): string[] {
const errors = [];
for (const functionDef of Object.values(manifest.functions)) {
if (!functionDef.codeWidget) {
continue;
}
if (typeof functionDef.codeWidget !== "string") {
errors.push(`Codewidgets require a string name.`);
}
}
return errors;
}
}

View File

@ -211,6 +211,22 @@
}
}
.sb-fenced-code-hide {
background-color: transparent !important;
line-height: 0;
}
.sb-fenced-code-iframe {
background-color: transparent !important;
iframe {
border: 0;
width: 100%;
padding: 0;
margin: 0;
}
}
.sb-line-blockquote {
border-left: 1px solid rgb(74, 74, 74);
}