diff --git a/client/codemirror/custom_syntax_widget.ts b/client/codemirror/custom_syntax_widget.ts index 5466b80c..e587c10f 100644 --- a/client/codemirror/custom_syntax_widget.ts +++ b/client/codemirror/custom_syntax_widget.ts @@ -21,10 +21,20 @@ export type CustomSyntaxExtension = CustomSyntaxSpec & { // CSS class applied to the rendered widget when cursor is outside the region renderClass?: string; // Callback function(body, pageName) returning widget content for live preview + renderWidget?: ( + body: string, + pageName: string, + ) => LuaWidgetContent | Promise; + // Deprecated: use renderWidget instead (kept for backwards compatibility) render?: ( body: string, pageName: string, ) => LuaWidgetContent | Promise; + // Callback function(body, pageName) returning HTML for markdown-to-HTML rendering + renderHtml?: ( + body: string, + pageName: string, + ) => string | HTMLElement | Promise; }; export function customSyntaxPlugin(client: Client) { @@ -84,7 +94,8 @@ export function customSyntaxPlugin(client: Client) { } // When cursor is outside and a render callback exists, show widget - if (spec.render && !isCursorInRange(state, [node.from, node.to])) { + const widgetRenderFn = spec.renderWidget ?? spec.render; + if (widgetRenderFn && !isCursorInRange(state, [node.from, node.to])) { const bodyText = bodyNode ? state.sliceDoc(bodyNode.from, bodyNode.to) : ""; @@ -99,7 +110,7 @@ export function customSyntaxPlugin(client: Client) { codeText, callback: async (body, pageName) => { try { - const result = await spec.render!(body, pageName); + const result = await widgetRenderFn(body, pageName); // Inject renderClass into widget content if configured if ( spec.renderClass && diff --git a/client/codemirror/inline_content.ts b/client/codemirror/inline_content.ts index 0f0a6aa6..e4a0085d 100644 --- a/client/codemirror/inline_content.ts +++ b/client/codemirror/inline_content.ts @@ -90,6 +90,12 @@ export function inlineContentPlugin(client: Client) { nameFromTransclusion(transclusion), parseMarkdown(result.text, result.offset), client.clientSystem.spaceLuaEnv, + { + syntaxExtensions: client.config.get( + "syntaxExtensions", + {}, + ), + }, ), ), }; diff --git a/client/codemirror/lua_widget.ts b/client/codemirror/lua_widget.ts index bf4dcc6b..7fa6bb0b 100644 --- a/client/codemirror/lua_widget.ts +++ b/client/codemirror/lua_widget.ts @@ -2,8 +2,8 @@ import { WidgetType } from "@codemirror/view"; import type { Client } from "../client.ts"; import { renderMarkdownToHtml } from "../markdown_renderer/markdown_render.ts"; import { parse } from "../markdown_parser/parse_tree.ts"; -import { extendedMarkdownLanguage } from "../markdown_parser/parser.ts"; -import { renderToText } from "@silverbulletmd/silverbullet/lib/tree"; +import { buildExtendedMarkdownLanguage } from "../markdown_parser/parser.ts"; +import { type ParseTree, renderToText } from "@silverbulletmd/silverbullet/lib/tree"; import { attachWidgetEventHandlers, buildTranslateUrls, @@ -108,10 +108,41 @@ export class LuaWidget extends WidgetType { return wrapperSpan; } + private get syntaxExtensions() { + return this.opts.client.config.get("syntaxExtensions", {}); + } + + // Parse and expand custom syntax in a markdown string (no transclusions/directives) + private async parseAndExpandCustomSyntax( + text: string, + pageName: string, + ): Promise { + const syntaxExtensions = this.syntaxExtensions; + const mdTree = parse( + buildExtendedMarkdownLanguage(syntaxExtensions), + text, + ); + return expandMarkdown( + this.opts.client.space, + pageName, + mdTree, + this.opts.client.clientSystem.spaceLuaEnv, + { + expandTransclusions: false, + expandLuaDirectives: false, + rewriteTasks: false, + syntaxExtensions, + }, + ); + } + // Build an inline Markdown renderer bound to the current client context - private buildInlineRenderer(): (text: string) => string { - return (text: string): string => { - const mdTree = parse(extendedMarkdownLanguage, text); + private buildInlineRenderer(): (text: string) => Promise { + return async (text: string): Promise => { + const mdTree = await this.parseAndExpandCustomSyntax( + text, + this.opts.client.currentName(), + ); return renderMarkdownToHtml( mdTree, { @@ -197,7 +228,8 @@ export class LuaWidget extends WidgetType { } } if (wc.markdown) { - let mdTree = parse(extendedMarkdownLanguage, wc.markdown || ""); + const syntaxExtensions = this.syntaxExtensions; + let mdTree = parse(buildExtendedMarkdownLanguage(syntaxExtensions), wc.markdown || ""); mdTree = await expandMarkdown( this.opts.client.space, @@ -206,6 +238,7 @@ export class LuaWidget extends WidgetType { this.opts.client.clientSystem.spaceLuaEnv, { rewriteTasks: false, + syntaxExtensions, }, ); const trimmedMarkdown = renderToText(mdTree).trim(); @@ -235,8 +268,10 @@ export class LuaWidget extends WidgetType { div.className += " sb-lua-directive-inline"; } - // Parse the markdown again after trimming - mdTree = parse(extendedMarkdownLanguage, trimmedMarkdown); + mdTree = await this.parseAndExpandCustomSyntax( + trimmedMarkdown, + currentName, + ); html = parseHtmlString( renderMarkdownToHtml( diff --git a/client/codemirror/table.ts b/client/codemirror/table.ts index 39259cf9..8b8b5a34 100644 --- a/client/codemirror/table.ts +++ b/client/codemirror/table.ts @@ -57,6 +57,9 @@ class TableViewWidget extends WidgetType { this.client.currentName(), this.t, this.client.clientSystem.spaceLuaEnv, + { + syntaxExtensions: this.client.config.get("syntaxExtensions", {}), + }, ).then((t) => { dom.innerHTML = renderMarkdownToHtml(t, { // Annotate every element with its position so we can use it to put diff --git a/client/markdown_renderer/html_render.test.ts b/client/markdown_renderer/html_render.test.ts index ef4ff3b0..ed7ae173 100644 --- a/client/markdown_renderer/html_render.test.ts +++ b/client/markdown_renderer/html_render.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import { renderHtml } from "./html_render.ts"; +import { Fragment, RawHtml, renderHtml } from "./html_render.ts"; test("HTML Render", () => { expect( @@ -24,3 +24,36 @@ test("HTML Render", () => { }), ).toEqual(`<>`); }); + +test("RawHtml with string body passes through unescaped", () => { + expect( + renderHtml({ + name: RawHtml, + body: 'italic & "quoted"', + }), + ).toEqual('italic & "quoted"'); +}); + +test("RawHtml with array body passes strings through unescaped", () => { + expect( + renderHtml({ + name: RawHtml, + body: [ + "bold", + { name: "i", body: "normal" }, + ], + }), + ).toEqual("boldnormal"); +}); + +test("Fragment renders children without wrapper", () => { + expect( + renderHtml({ + name: Fragment, + body: [ + { name: "b", body: "one" }, + { name: "i", body: "two" }, + ], + }), + ).toEqual("onetwo"); +}); diff --git a/client/markdown_renderer/html_render.ts b/client/markdown_renderer/html_render.ts index 293d133a..49a1175c 100644 --- a/client/markdown_renderer/html_render.ts +++ b/client/markdown_renderer/html_render.ts @@ -1,4 +1,5 @@ export const Fragment = "FRAGMENT"; +export const RawHtml = "RAW_HTML"; export type Tag = | { @@ -48,5 +49,13 @@ export function renderHtml(t: Tag | null): string { if (t.name === Fragment) { return body; } + if (t.name === RawHtml) { + if (typeof t.body === "string") { + return t.body; + } + return t.body.map((c) => + typeof c === "string" ? c : renderHtml(c) + ).join(""); + } return `<${t.name}${attrs}>${body}`; } diff --git a/client/markdown_renderer/inline.ts b/client/markdown_renderer/inline.ts index 4a17eb6e..49a84f8d 100644 --- a/client/markdown_renderer/inline.ts +++ b/client/markdown_renderer/inline.ts @@ -5,6 +5,7 @@ import { renderToText, replaceNodesMatchingAsync, } from "@silverbulletmd/silverbullet/lib/tree"; +import { htmlEscape } from "./html_render.ts"; import { getPathExtension, isMarkdownPath, @@ -16,7 +17,9 @@ import { } from "@silverbulletmd/silverbullet/lib/resolve"; import mime from "mime"; import { LuaStackFrame, LuaTable } from "../space_lua/runtime.ts"; -import { parseMarkdown } from "../markdown_parser/parser.ts"; +import { buildExtendedMarkdownLanguage } from "../markdown_parser/parser.ts"; +import type { CustomSyntaxSpec } from "../markdown_parser/custom_syntax.ts"; +import { parse } from "../markdown_parser/parse_tree.ts"; import { renderExpressionResult } from "./result_render.ts"; import { parseExpressionString } from "../space_lua/parse.ts"; import { evalExpression } from "../space_lua/eval.ts"; @@ -31,6 +34,17 @@ import { import type { Space } from "../space.ts"; import type { SpaceLuaEnvironment } from "../space_lua.ts"; +// Synthetic node type used to represent pre-resolved custom syntax HTML in the parse tree +export const CustomSyntaxRenderedHtmlType = "CustomSyntaxRenderedHtml"; + +// Extends the parser spec with an optional renderHtml callback for HTML rendering +export type CustomSyntaxHtmlRenderer = CustomSyntaxSpec & { + renderHtml?: ( + body: string, + pageName: string, + ) => string | HTMLElement | Promise; +}; + export type MarkdownExpandOptions = { // all options default to true, set to false to explicitly disable // Replace (markdown transclusions) with their content @@ -39,6 +53,8 @@ export type MarkdownExpandOptions = { expandLuaDirectives?: boolean; // Rewrite tasks to include references so that they can be updated rewriteTasks?: boolean; + // Custom syntax extensions keyed by name, with optional renderHtml callbacks + syntaxExtensions?: Record; }; /** @@ -54,6 +70,7 @@ export async function expandMarkdown( options: MarkdownExpandOptions = {}, processedPages: Set = new Set(), ): Promise { + const mdLang = buildExtendedMarkdownLanguage(options.syntaxExtensions); addParentPointers(mdTree); await replaceNodesMatchingAsync(mdTree, async (n) => { if (n.type === "Image" && options.expandTransclusions !== false) { @@ -89,7 +106,7 @@ export async function expandMarkdown( // it so we won't touch it down the line and cause endless recursion processedPages.add(transclusion.url); - const tree = parseMarkdown(result.text); + const tree = parse(mdLang, result.text); // Recursively process return expandMarkdown( @@ -101,7 +118,7 @@ export async function expandMarkdown( processedPages, ); } catch (e: any) { - return parseMarkdown(`**Error:** ${e.message}`); + return parse(mdLang, `**Error:** ${e.message}`); } } else if ( n.type === "LuaDirective" && @@ -130,11 +147,11 @@ export async function expandMarkdown( } else if (result instanceof LuaTable && result.has("markdown")) { result = result.get("markdown"); } - return parseMarkdown(await renderExpressionResult(result)); + return parse(mdLang, await renderExpressionResult(result)); } catch (e: any) { // Reduce blast radius and give useful error message console.error("Error evaluating Lua directive", exprText, e); - return parseMarkdown(`**Error:** ${e.message}`); + return parse(mdLang, `**Error:** ${e.message}`); } } else if (n.type === "Task" && options.rewriteTasks !== false) { // Add a task reference to this based on the current page name if there's not one already @@ -165,6 +182,30 @@ export async function expandMarkdown( }, ); } + } else if (n.type && options.syntaxExtensions) { + // Resolve custom syntax renderHtml callbacks + const spec = options.syntaxExtensions[n.type]; + if (!spec?.renderHtml) return; + + const bodyNode = findNodeOfType(n, `${spec.name}Body`); + const bodyText = bodyNode ? renderToText(bodyNode) : ""; + + try { + let result = await spec.renderHtml(bodyText, pageName); + if (typeof result !== "string" && "outerHTML" in result) { + result = result.outerHTML; + } + return { + type: CustomSyntaxRenderedHtmlType, + children: [{ text: result }], + }; + } catch (e: any) { + console.error(`Error in ${spec.name} renderHtml:`, e); + return { + type: CustomSyntaxRenderedHtmlType, + children: [{ text: `Error in ${htmlEscape(spec.name)} renderHtml: ${htmlEscape(e.message)}` }], + }; + } } }); return mdTree; diff --git a/client/markdown_renderer/markdown_render.test.ts b/client/markdown_renderer/markdown_render.test.ts index 2db3cda6..86e3f235 100644 --- a/client/markdown_renderer/markdown_render.test.ts +++ b/client/markdown_renderer/markdown_render.test.ts @@ -2,7 +2,14 @@ import { expect, test } from "vitest"; import { parse } from "../markdown_parser/parse_tree.ts"; import { renderMarkdownToHtml } from "./markdown_render.ts"; -import { extendedMarkdownLanguage } from "../markdown_parser/parser.ts"; +import { + buildExtendedMarkdownLanguage, + extendedMarkdownLanguage, +} from "../markdown_parser/parser.ts"; +import { CustomSyntaxRenderedHtmlType, expandMarkdown } from "./inline.ts"; +import type { Space } from "../space.ts"; +import type { SpaceLuaEnvironment } from "../space_lua.ts"; +import { LuaEnv } from "../space_lua/runtime.ts"; const sampleMarkdown = `--- name: Sup @@ -189,3 +196,159 @@ test("Unmatched HTML tags render as literal text", () => { 'text <b>unclosed', ); }); + +test("CustomSyntaxRenderedHtml renders raw HTML", () => { + // Directly test the renderer with a synthetic parse tree + const tree = { + type: "Document", + children: [ + { + type: "Paragraph", + children: [ + { text: "Before " }, + { + type: CustomSyntaxRenderedHtmlType, + children: [{ text: "rendered" }], + }, + { text: " after" }, + ], + }, + ], + }; + const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); + expect(html).toEqual( + 'Before rendered after', + ); +}); + +// Minimal stubs for expandMarkdown tests +const stubSpace = {} as Space; +const stubSle = { env: new LuaEnv() } as SpaceLuaEnvironment; +const defaultExpandOpts = { + expandTransclusions: false, + expandLuaDirectives: false, + rewriteTasks: false, +}; + +const latexInlineSpec = { + name: "LatexInline", + startMarker: "\\$", + endMarker: "\\$", + mode: "inline" as const, +}; +const latexBlockSpec = { + name: "LatexBlock", + startMarker: "^\\$\\$$", + endMarker: "^\\$\\$$", + mode: "block" as const, +}; +const customSpec = { + name: "Custom", + startMarker: "<<", + endMarker: ">>", + mode: "inline" as const, +}; + +test("expandMarkdown resolves inline custom syntax renderHtml", async () => { + const lang = buildExtendedMarkdownLanguage({ + LatexInline: latexInlineSpec, + }); + const tree = parse(lang, "Hello $E=mc^2$ world"); + + const expanded = await expandMarkdown(stubSpace, "test", tree, stubSle, { + ...defaultExpandOpts, + syntaxExtensions: { + LatexInline: { + ...latexInlineSpec, + renderHtml: (body, _pageName) => `${body}`, + }, + }, + }); + + const html = renderMarkdownToHtml(expanded, { failOnUnknown: true }); + expect(html).toEqual( + 'Hello E=mc^2 world', + ); +}); + +test("expandMarkdown resolves block custom syntax renderHtml", async () => { + const lang = buildExtendedMarkdownLanguage({ + LatexBlock: latexBlockSpec, + }); + const tree = parse(lang, "$$\nE=mc^2\n$$"); + + const expanded = await expandMarkdown(stubSpace, "test", tree, stubSle, { + ...defaultExpandOpts, + syntaxExtensions: { + LatexBlock: { + ...latexBlockSpec, + renderHtml: (body, _pageName) => + `
${body.trim()}
`, + }, + }, + }); + + const html = renderMarkdownToHtml(expanded, { failOnUnknown: true }); + expect(html).toEqual('
E=mc^2
'); +}); + +test("expandMarkdown passes pageName to renderHtml", async () => { + const lang = buildExtendedMarkdownLanguage({ Custom: customSpec }); + const tree = parse(lang, "Hello <> world"); + + let receivedPageName: string | undefined; + await expandMarkdown(stubSpace, "MyPage", tree, stubSle, { + ...defaultExpandOpts, + syntaxExtensions: { + Custom: { + ...customSpec, + renderHtml: (_body, pageName) => { + receivedPageName = pageName; + return "ok"; + }, + }, + }, + }); + + expect(receivedPageName).toEqual("MyPage"); +}); + +test("expandMarkdown handles renderHtml errors gracefully", async () => { + const lang = buildExtendedMarkdownLanguage({ Custom: customSpec }); + const tree = parse(lang, "Hello <> world"); + + const expanded = await expandMarkdown(stubSpace, "test", tree, stubSle, { + ...defaultExpandOpts, + syntaxExtensions: { + Custom: { + ...customSpec, + renderHtml: () => { + throw new Error("something failed"); + }, + }, + }, + }); + + const html = renderMarkdownToHtml(expanded); + expect(html).toContain("Error in Custom renderHtml: something failed"); + expect(html).toContain(''); +}); + +test("expandMarkdown skips custom syntax without renderHtml", async () => { + const lang = buildExtendedMarkdownLanguage({ Custom: customSpec }); + const tree = parse(lang, "Hello <> world"); + + const expanded = await expandMarkdown(stubSpace, "test", tree, stubSle, { + ...defaultExpandOpts, + syntaxExtensions: { + Custom: { + ...customSpec, + // No renderHtml callback + }, + }, + }); + + const html = renderMarkdownToHtml(expanded); + // Should fall through to default rendering (raw text, HTML-escaped) + expect(html).toContain("<<content>>"); +}); diff --git a/client/markdown_renderer/markdown_render.ts b/client/markdown_renderer/markdown_render.ts index 5ea3bbdf..1976a104 100644 --- a/client/markdown_renderer/markdown_render.ts +++ b/client/markdown_renderer/markdown_render.ts @@ -13,7 +13,8 @@ import { encodeRef, parseToRef, } from "@silverbulletmd/silverbullet/lib/ref"; -import { Fragment, renderHtml, type Tag } from "./html_render.ts"; +import { Fragment, RawHtml, renderHtml, type Tag } from "./html_render.ts"; +import { CustomSyntaxRenderedHtmlType } from "./inline.ts"; import * as TagConstants from "../../plugs/index/constants.ts"; import { extractHashtag } from "@silverbulletmd/silverbullet/lib/tags"; import { justifiedTableRender } from "./justified_tables.ts"; @@ -557,6 +558,12 @@ function render(t: ParseTree, options: MarkdownRenderOptions = {}): Tag | null { ], }; } + case CustomSyntaxRenderedHtmlType: + return { + name: RawHtml, + body: renderToText(t), + }; + case "LuaDirective": return { name: "span", diff --git a/client/plugos/syscalls/markdown.ts b/client/plugos/syscalls/markdown.ts index b9543870..18e37575 100644 --- a/client/plugos/syscalls/markdown.ts +++ b/client/plugos/syscalls/markdown.ts @@ -4,7 +4,7 @@ import { type ParseTree, renderToText, } from "@silverbulletmd/silverbullet/lib/tree"; -import { extendedMarkdownLanguage } from "../../markdown_parser/parser.ts"; +import { buildExtendedMarkdownLanguage } from "../../markdown_parser/parser.ts"; import { expandMarkdown, type MarkdownExpandOptions, @@ -22,7 +22,7 @@ import { export function markdownSyscalls(client: Client): SysCallMapping { return { "markdown.parseMarkdown": (_ctx, text: string): ParseTree => { - return parse(extendedMarkdownLanguage, text); + return parse(markdownLanguageWithUserExtensions(client), text); }, "markdown.renderParseTree": (_ctx, tree: ParseTree): string => { return renderToText(tree); @@ -34,7 +34,10 @@ export function markdownSyscalls(client: Client): SysCallMapping { ): Promise => { const outputString = typeof treeOrText === "string"; if (typeof treeOrText === "string") { - treeOrText = parse(extendedMarkdownLanguage, treeOrText); + treeOrText = parse( + markdownLanguageWithUserExtensions(client), + treeOrText, + ); } const result = await expandMarkdownWithClient( client, @@ -52,7 +55,7 @@ export function markdownSyscalls(client: Client): SysCallMapping { text: string, options: MarkdownRenderOptions = {}, ) => { - let mdTree = parse(extendedMarkdownLanguage, text); + let mdTree = parse(markdownLanguageWithUserExtensions(client), text); if (options.expand) { mdTree = await expandMarkdownWithClient(client, mdTree); } @@ -70,6 +73,12 @@ export function markdownSyscalls(client: Client): SysCallMapping { }; } +function markdownLanguageWithUserExtensions(client: Client) { + return buildExtendedMarkdownLanguage( + client.config.get("syntaxExtensions", {}), + ); +} + function expandMarkdownWithClient( client: Client, tree: ParseTree, @@ -80,6 +89,9 @@ function expandMarkdownWithClient( client.currentName(), tree, client.clientSystem.spaceLuaEnv, - options, + { + ...options, + syntaxExtensions: client.config.get("syntaxExtensions", {}), + }, ); } diff --git a/client/space_lua/render_lua_html.ts b/client/space_lua/render_lua_html.ts index 773e7cdf..c3867a65 100644 --- a/client/space_lua/render_lua_html.ts +++ b/client/space_lua/render_lua_html.ts @@ -11,7 +11,7 @@ function escapeHtml(s: string): string { } // Render inline Markdown to HTML (caller provides the implementation) -export type InlineRenderer = (text: string) => string; +export type InlineRenderer = (text: string) => string | Promise; function defaultInlineRenderer(text: string): string { return escapeHtml(text); @@ -80,7 +80,7 @@ export async function renderResultToHtml( } if (typeof result === "string") { return { - html: renderInline(result), + html: await renderInline(result), dataType: "string", }; } @@ -144,7 +144,7 @@ export async function renderResultToHtml( }; } return { - html: renderInline(`${result}`), + html: await renderInline(`${result}`), dataType: "string", }; } @@ -308,13 +308,13 @@ async function renderLiHtml( } // Render a value content (recurse for nested tables/arrays) -function renderCellContent( +async function renderCellContent( v: any, renderInline: InlineRenderer, ): Promise { if (isEmpty(v)) return Promise.resolve(""); if (isTaggedFloat(v)) { - return Promise.resolve(renderInline(luaFormatNumber(v.value, "float"))); + return renderInline(luaFormatNumber(v.value, "float")); } if (v instanceof LuaTable) { if (v.empty()) return Promise.resolve(`
`); @@ -335,5 +335,5 @@ function renderCellContent( } return renderJsObjectArrayToHtml([v], renderInline); } - return Promise.resolve(renderInline(formatScalar(v))); + return renderInline(formatScalar(v)); } diff --git a/libraries/Library/Std/APIs/Syntax.md b/libraries/Library/Std/APIs/Syntax.md index 330b4a1b..adf95378 100644 --- a/libraries/Library/Std/APIs/Syntax.md +++ b/libraries/Library/Std/APIs/Syntax.md @@ -16,7 +16,8 @@ Available keys: * `bodyClass`: (optional) CSS class for the body content between markers * `endMarkerClass`: (optional) CSS class for the end marker * `renderClass`: (optional) CSS class applied to the rendered widget -* `render`: (optional) Callback `function(body, pageName)` returning widget content for live preview +* `renderWidget`: (optional) Callback `function(body, pageName)` returning widget content for live editor preview. Previously named `render`, which is still supported for backwards compatibility. +* `renderHtml`: Callback `function(body, pageName)` returning an HTML string or `HTMLElement` (e.g. via `dom.*`) for use when rendering to HTML. # Implementation ```space-lua diff --git a/website/API/syntax.md b/website/API/syntax.md index ccddec00..89acf17b 100644 --- a/website/API/syntax.md +++ b/website/API/syntax.md @@ -17,7 +17,8 @@ Registers a custom syntax extension. The extension will be active after the next * `endMarkerClass`: CSS class for the end marker * CSS styling applied when Live Previewing: * `renderClass`: CSS class applied to the rendered widget -* `render` (required): Callback `function(body, pageName)` returning [[API/widget|widget]] content for live preview. +* `renderWidget`: Callback `function(body, pageName)` returning [[API/widget|widget]] content for Live Preview. +* `renderHtml`: Callback `function(body, pageName)` returning an HTML string or `HTMLElement` (e.g. via `dom.*`) for use when rendering to HTML. # Escaping Within the body of an inline syntax extension, backslash (`\`) acts as an escape character. Use `\` before the end marker to include it literally in the body. The render callback receives the **raw** body text with escape sequences intact; the renderer is responsible for unescaping if needed. @@ -35,8 +36,11 @@ syntax.define { --bodyClass = "sb-latex-body", --endMarkerClass = "sb-latex-mark", renderClass = "sb-latex-inline-preview", - render = function(body, pageName) + renderWidget = function(body, pageName) return widget.html(dom.i { body }) + end, + renderHtml = function(body, pageName) + return dom.i { body } end } ``` @@ -57,8 +61,11 @@ syntax.define { startMarkerClass = "sb-latex-mark", bodyClass = "sb-latex-body", endMarkerClass = "sb-latex-mark", - render = function(body, pageName) + renderWidget = function(body, pageName) return widget.htmlBlock(dom.marquee { body }) + end, + renderHtml = function(body, pageName) + return dom.marquee { body } end } ``` @@ -77,4 +84,4 @@ You can style these with [[Space Style]]: .sb-latex-body { color: gray; } -``` \ No newline at end of file +```