diff --git a/common/markdown_util.ts b/common/markdown_util.ts index cba188ea..96f0e23b 100644 --- a/common/markdown_util.ts +++ b/common/markdown_util.ts @@ -90,6 +90,9 @@ export async function jsonToMDTable( } export function renderExpressionResult(result: any): Promise { + if (result === undefined || result === null) { + return Promise.resolve("nil"); + } if (result instanceof LuaTable) { result = result.toJS(); } diff --git a/common/space_lua/language_core_test.lua b/common/space_lua/language_core_test.lua index b2a1e859..29da05c4 100644 --- a/common/space_lua/language_core_test.lua +++ b/common/space_lua/language_core_test.lua @@ -20,6 +20,9 @@ assert(a + b == 3) assert("Hello " .. "world" == "Hello world") assertEqual([[Hello world]], "Hello world") assertEqual([==[Hello [[world]]!]==], "Hello [[world]]!") +-- First newline should be eliminated if present +assertEqual([==[ +Hello world]==], "Hello world") -- Various forms of function definitions function f1() diff --git a/common/space_lua/parse.ts b/common/space_lua/parse.ts index 82b162b3..52daffb2 100644 --- a/common/space_lua/parse.ts +++ b/common/space_lua/parse.ts @@ -342,7 +342,12 @@ function parseString(s: string): string { // Handle long strings with delimiters const delimiterMatch = s.match(delimiterRegex); if (delimiterMatch) { - return delimiterMatch[2]; + let text = delimiterMatch[2]; + // According to Lua semantics, whenever a [[ formatted string starts with a newline, that newline should be skipped + if (text[0] === "\n") { + text = text.slice(1); + } + return text; } return s.slice(1, -1).replace( /\\(x[0-9a-fA-F]{2}|u\{[0-9a-fA-F]+\}|[abfnrtv\\'"n])/g, diff --git a/plug-api/types.ts b/plug-api/types.ts index 2d987226..735a82bf 100644 --- a/plug-api/types.ts +++ b/plug-api/types.ts @@ -147,11 +147,12 @@ export type AppEvent = | "editor:pageReloaded" | "editor:pageSaving" | "editor:pageSaved" + | "editor:pageCreating" + | "editor:pageModified" | "editor:documentSaving" | "editor:documentSaved" | "editor:modeswitch" | "plugs:loaded" - | "editor:pageModified" | "hooks:renderTopWidgets" | "hooks:renderBottomWidgets"; @@ -195,6 +196,15 @@ export type CompleteEvent = { parentNodes: string[]; }; +export type PageCreatingEvent = { + name: string; +}; + +export type PageCreatingContent = { + text: string; + perm: "ro" | "rw"; +}; + export type SlashCompletionOption = { label: string; detail?: string; diff --git a/plugs/core/Library/Std/Query Templates.md b/plugs/core/Library/Std/Query Templates.md index d62eb629..54e1cc1d 100644 --- a/plugs/core/Library/Std/Query Templates.md +++ b/plugs/core/Library/Std/Query Templates.md @@ -12,6 +12,15 @@ templates.pageItem = template.new([==[ templates.taskItem = template.new([==[ * [${state}] [[${ref}]] ${name} ]==]) + +-- Renders a task object as a togglable task +templates.itemItem = template.new([==[ +* ${name} +]==]) + +templates.paragraphItem = template.new([==[ +* ${name} +]==]) ``` diff --git a/plugs/core/Library/Std/Widgets.md b/plugs/core/Library/Std/Widgets.md index 22c22a9a..985525ff 100644 --- a/plugs/core/Library/Std/Widgets.md +++ b/plugs/core/Library/Std/Widgets.md @@ -86,7 +86,7 @@ function widgets.linkedMentions() ]] if #linkedMentions > 0 then return widget.new { - markdown = "# Linked Mentions" + markdown = "# Linked Mentions\n" .. template.each(linkedMentions, mentionTemplate) } end diff --git a/plugs/index/constants.ts b/plugs/index/constants.ts index 710befb0..0152fac3 100644 --- a/plugs/index/constants.ts +++ b/plugs/index/constants.ts @@ -1 +1 @@ -export const tagPrefix = "📌 "; +export const tagPrefix = "tag:"; diff --git a/plugs/index/index.plug.yaml b/plugs/index/index.plug.yaml index ba58e4a7..30d19aec 100644 --- a/plugs/index/index.plug.yaml +++ b/plugs/index/index.plug.yaml @@ -188,25 +188,6 @@ functions: events: - editor:lint - # Tag file system - readFileTag: - path: tag_page.ts:readFileTag - pageNamespace: - pattern: "📌 .+" - operation: readFile - - writePageTag: - path: tag_page.ts:writeFileTag - pageNamespace: - pattern: "📌 .+" - operation: readFile - - getPageMetaTag: - path: tag_page.ts:getFileMetaTag - pageNamespace: - pattern: "📌 .+" - operation: getFileMeta - config: # Schema for the built-in tags indexed by this plug schema.tag: diff --git a/plugs/index/tag_page.ts b/plugs/index/tag_page.ts deleted file mode 100644 index 9b412bec..00000000 --- a/plugs/index/tag_page.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { FileMeta } from "../../plug-api/types.ts"; -import { renderHashtag } from "../../plug-api/lib/tags.ts"; -import { markdown, system } from "@silverbulletmd/silverbullet/syscalls"; -import { renderToText } from "@silverbulletmd/silverbullet/lib/tree"; -import { tagPrefix } from "./constants.ts"; - -export async function readFileTag( - name: string, -): Promise<{ data: Uint8Array; meta: FileMeta }> { - const tagName = name.substring( - tagPrefix.length, - name.length - ".md".length, - ); - const text = `All objects in your space tagged with ${renderHashtag(tagName)}: -\`\`\`template -template: | - {{#if .}} - # Pages - {{#each .}} - * [[{{name}}]] - {{/each}} - {{/if}} -query: | - page where tags = "${tagName}" -\`\`\` -\`\`\`template -template: | - {{#if .}} - # Tasks - {{#each .}} - * [{{state}}] [[{{ref}}]] {{text}} - {{/each}} - {{/if}} -query: | - task where tags = "${tagName}" -\`\`\` -\`\`\`template -template: | - {{#if .}} - # Items - {{#each .}} - * [[{{ref}}]] {{text}} - {{/each}} - {{/if}} -query: | - item where tags = "${tagName}" -\`\`\` -\`\`\`template -template: | - {{#if .}} - # Paragraphs - {{#each .}} - * [[{{ref}}]] {{text}} - {{/each}} - {{/if}} -query: | - paragraph where tags = "${tagName}" -\`\`\` -`; - - let tree = await markdown.parseMarkdown(text); - tree = await system.invokeFunction("markdown.expandCodeWidgets", tree, name); - - return { - data: new TextEncoder().encode(renderToText(tree)), - meta: { - name, - contentType: "text/markdown", - size: text.length, - created: 0, - lastModified: 0, - perm: "ro", - }, - }; -} - -export function writeFileTag( - name: string, -): FileMeta { - // Never actually writing this - return getFileMetaTag(name); -} - -export function getFileMetaTag(name: string): FileMeta { - return { - name, - contentType: "text/markdown", - size: -1, - created: 0, - lastModified: 0, - perm: "ro", - }; -} diff --git a/plugs/search/search.plug.yaml b/plugs/search/search.plug.yaml index bb27ec39..f2745e5f 100644 --- a/plugs/search/search.plug.yaml +++ b/plugs/search/search.plug.yaml @@ -14,18 +14,5 @@ functions: readPageSearch: path: ./search.ts:readFileSearch - pageNamespace: - pattern: "🔍 .+" - operation: readFile - - writePageSearch: - path: ./search.ts:writeFileSearch - pageNamespace: - pattern: "🔍 .+" - operation: readFile - - getPageMetaSearch: - path: ./search.ts:getFileMetaSearch - pageNamespace: - pattern: "🔍 .+" - operation: getFileMeta + events: + - editor:pageCreating diff --git a/plugs/search/search.ts b/plugs/search/search.ts index 10052df1..0c5ab129 100644 --- a/plugs/search/search.ts +++ b/plugs/search/search.ts @@ -1,11 +1,14 @@ -import type { IndexTreeEvent } from "../../plug-api/types.ts"; +import type { + IndexTreeEvent, + PageCreatingContent, + PageCreatingEvent, +} from "../../plug-api/types.ts"; import { renderToText } from "@silverbulletmd/silverbullet/lib/tree"; import { editor } from "@silverbulletmd/silverbullet/syscalls"; -import type { FileMeta } from "../../plug-api/types.ts"; import { ftsIndexPage, ftsSearch } from "./engine.ts"; import { PromiseQueue } from "$lib/async.ts"; -const searchPrefix = "🔍 "; +const searchPrefix = "search:"; // Search indexing is prone to concurrency issues, so we queue all write operations const promiseQueue = new PromiseQueue(); @@ -27,12 +30,12 @@ export async function searchCommand() { } export async function readFileSearch( - name: string, -): Promise<{ data: Uint8Array; meta: FileMeta }> { - const phrase = name.substring( - searchPrefix.length, - name.length - ".md".length, - ); + { name }: PageCreatingEvent, +): Promise { + if (!name.startsWith(searchPrefix)) { + return; + } + const phrase = name.substring(searchPrefix.length); const results = await ftsSearch(phrase); const text = `# Search results for "${phrase}"\n${ results @@ -42,32 +45,7 @@ export async function readFileSearch( `; return { - data: new TextEncoder().encode(text), - meta: { - name, - contentType: "text/markdown", - size: text.length, - created: 0, - lastModified: 0, - perm: "ro", - }, - }; -} - -export function writeFileSearch( - name: string, -): FileMeta { - // Never actually writing this - return getFileMetaSearch(name); -} - -export function getFileMetaSearch(name: string): FileMeta { - return { - name, - contentType: "text/markdown", - size: -1, - created: 0, - lastModified: 0, + text, perm: "ro", }; } diff --git a/web/client.ts b/web/client.ts index 356652fa..56987ebd 100644 --- a/web/client.ts +++ b/web/client.ts @@ -25,6 +25,8 @@ import type { CompleteEvent, DocumentMeta, EnrichedClickEvent, + PageCreatingContent, + PageCreatingEvent, SlashCompletions, } from "../plug-api/types.ts"; import type { StyleObject } from "../plugs/index/style.ts"; @@ -220,11 +222,7 @@ export class Client { // Load plugs await this.loadPlugs(); - // Asynchronously load the space scripts - this.clientSystem.loadSpaceScripts().catch((e) => { - console.error("Error loading space scripts", e); - }); - + await this.clientSystem.loadSpaceScripts(); await this.initNavigator(); await this.initSync(); await this.eventHook.dispatchEvent("system:ready"); @@ -1222,6 +1220,20 @@ export class Client { perm: "rw", } as PageMeta, }; + + // Let's dispatch a editor:pageCreating event to see if anybody wants to do something before the page is created + const results = await this.dispatchAppEvent( + "editor:pageCreating", + { name: pageName } as PageCreatingEvent, + ) as PageCreatingContent[]; + if (results.length === 1) { + doc.text = results[0].text; + doc.meta.perm = results[0].perm; + } else if (results.length > 1) { + console.error( + "Multiple responses for editor:pageCreating event, this is not supported", + ); + } } else { this.flashNotification( `Could not load page ${pageName}: ${e.message}`,