parent
2e6a938c01
commit
fb21377bef
|
@ -1,8 +1,8 @@
|
||||||
import { FunctionMap } from "../plug-api/types.ts";
|
import { FunctionMap, Query } from "$sb/types.ts";
|
||||||
import { builtinFunctions } from "../lib/builtin_query_functions.ts";
|
import { builtinFunctions } from "$lib/builtin_query_functions.ts";
|
||||||
import { System } from "../lib/plugos/system.ts";
|
import { System } from "$lib/plugos/system.ts";
|
||||||
import { Query } from "../plug-api/types.ts";
|
|
||||||
import { LimitedMap } from "$lib/limited_map.ts";
|
import { LimitedMap } from "$lib/limited_map.ts";
|
||||||
|
import { parsePageRef } from "$sb/lib/page_ref.ts";
|
||||||
|
|
||||||
const pageCacheTtl = 10 * 1000; // 10s
|
const pageCacheTtl = 10 * 1000; // 10s
|
||||||
|
|
||||||
|
@ -49,20 +49,58 @@ export function buildQueryFunctions(
|
||||||
variables,
|
variables,
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
// INTERNAL: Used to implement resolving [[links]] in expressions
|
// INTERNAL: Used to implement resolving [[links]] in expressions, also supports [[link#header]] and [[link$pos]] as well as [[link$anchor]]
|
||||||
readPage(name: string): Promise<string> | string {
|
async readPage(name: string): Promise<string> {
|
||||||
const cachedPage = pageCache.get(name);
|
const cachedPage = pageCache.get(name);
|
||||||
if (cachedPage) {
|
if (cachedPage) {
|
||||||
return cachedPage;
|
return cachedPage;
|
||||||
} else {
|
} else {
|
||||||
return system.localSyscall("space.readPage", [name]).then((page) => {
|
const pageRef = parsePageRef(name);
|
||||||
|
try {
|
||||||
|
let page: string = await system.localSyscall("space.readPage", [
|
||||||
|
pageRef.page,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Extract page section if pos, anchor, or header are included
|
||||||
|
if (pageRef.pos) {
|
||||||
|
// If the page link includes a position, slice the page from that position
|
||||||
|
page = page.slice(pageRef.pos);
|
||||||
|
} else if (pageRef.anchor) {
|
||||||
|
// If the page link includes an anchor, slice the page from that anchor
|
||||||
|
const pos = page.indexOf(`$${pageRef.anchor}`);
|
||||||
|
page = page.slice(pos);
|
||||||
|
} else if (pageRef.header) {
|
||||||
|
// If the page link includes a header, select that header (up to the next header at the same level)
|
||||||
|
// Note: this an approximation, should ideally use the AST
|
||||||
|
let pos = page.indexOf(`# ${pageRef.header}\n`);
|
||||||
|
let headingLevel = 1;
|
||||||
|
while (page.charAt(pos - 1) === "#") {
|
||||||
|
pos--;
|
||||||
|
headingLevel++;
|
||||||
|
}
|
||||||
|
page = page.slice(pos);
|
||||||
|
|
||||||
|
// Slice up to the next equal or higher level heading
|
||||||
|
const headRegex = new RegExp(
|
||||||
|
`[^#]#{1,${headingLevel}} [^\n]*\n`,
|
||||||
|
"g",
|
||||||
|
);
|
||||||
|
const endPos = page.slice(headingLevel).search(headRegex) +
|
||||||
|
headingLevel;
|
||||||
|
if (endPos) {
|
||||||
|
page = page.slice(0, endPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pageCache.set(name, page, pageCacheTtl);
|
pageCache.set(name, page, pageCacheTtl);
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
}).catch((e: any) => {
|
} catch (e: any) {
|
||||||
if (e.message === "Not found") {
|
if (e.message === "Not found") {
|
||||||
throw new Error(`Page not found: ${name}`);
|
throw new Error(`Page not found: ${name}`);
|
||||||
}
|
}
|
||||||
});
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,8 @@ import {
|
||||||
MarkdownRenderOptions,
|
MarkdownRenderOptions,
|
||||||
renderMarkdownToHtml,
|
renderMarkdownToHtml,
|
||||||
} from "./markdown_render.ts";
|
} from "./markdown_render.ts";
|
||||||
|
import { validatePageName } from "$sb/lib/page_ref.ts";
|
||||||
|
import { parsePageRef } from "$sb/lib/page_ref.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds code widgets, runs their plug code to render and inlines their content in the parse tree
|
* Finds code widgets, runs their plug code to render and inlines their content in the parse tree
|
||||||
|
@ -58,6 +60,48 @@ export async function expandCodeWidgets(
|
||||||
console.error("Error rendering code", e.message);
|
console.error("Error rendering code", e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (n.type === "Image") {
|
||||||
|
// Let's scan for ![[embeds]] that are codified as Images, confusingly
|
||||||
|
const wikiLinkMark = findNodeOfType(n, "WikiLinkMark");
|
||||||
|
if (!wikiLinkMark) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const wikiLinkPage = findNodeOfType(n, "WikiLinkPage");
|
||||||
|
if (!wikiLinkPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = wikiLinkPage.children![0].text!;
|
||||||
|
|
||||||
|
// Check if this is likely a page link (based on the path format, e.g. if it contains an extension, it's probably not a page link)
|
||||||
|
try {
|
||||||
|
const ref = parsePageRef(page);
|
||||||
|
validatePageName(ref.page);
|
||||||
|
} catch {
|
||||||
|
// Not a valid page name, so not a page reference
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internally translate this to a template that inlines a page, then render that
|
||||||
|
const result = await codeWidget.render(
|
||||||
|
"template",
|
||||||
|
`{{[[${page}]]}}`,
|
||||||
|
page,
|
||||||
|
);
|
||||||
|
if (!result) {
|
||||||
|
return {
|
||||||
|
text: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Only do this for "markdown" widgets, that is: that can render to markdown
|
||||||
|
if (result.markdown !== undefined) {
|
||||||
|
const parsedBody = await parseMarkdown(result.markdown);
|
||||||
|
// Recursively process
|
||||||
|
return expandCodeWidgets(
|
||||||
|
parsedBody,
|
||||||
|
page,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return mdTree;
|
return mdTree;
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import { EditorState, Range } from "@codemirror/state";
|
import { EditorState, Range } from "@codemirror/state";
|
||||||
import { syntaxTree } from "@codemirror/language";
|
import { syntaxTree } from "@codemirror/language";
|
||||||
import { Decoration, WidgetType } from "@codemirror/view";
|
import { Decoration, WidgetType } from "@codemirror/view";
|
||||||
|
import { MarkdownWidget } from "./markdown_widget.ts";
|
||||||
import { decoratorStateField } from "./util.ts";
|
import { decoratorStateField } from "./util.ts";
|
||||||
import type { Client } from "../client.ts";
|
import type { Client } from "../client.ts";
|
||||||
import { isLocalPath, resolvePath } from "$sb/lib/resolve.ts";
|
import { isFederationPath, isLocalPath, resolvePath } from "$sb/lib/resolve.ts";
|
||||||
|
import { parsePageRef } from "$sb/lib/page_ref.ts";
|
||||||
|
|
||||||
type ImageDimensions = {
|
type ImageDimensions = {
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
class InlineImageWidget extends WidgetType {
|
class InlineContentWidget extends WidgetType {
|
||||||
constructor(
|
constructor(
|
||||||
readonly url: string,
|
readonly url: string,
|
||||||
readonly title: string,
|
readonly title: string,
|
||||||
|
@ -20,14 +22,13 @@ class InlineImageWidget extends WidgetType {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
eq(other: InlineImageWidget) {
|
eq(other: InlineContentWidget) {
|
||||||
return other.url === this.url && other.title === this.title &&
|
return other.url === this.url && other.title === this.title &&
|
||||||
JSON.stringify(other.dim) === JSON.stringify(this.dim);
|
JSON.stringify(other.dim) === JSON.stringify(this.dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
get estimatedHeight(): number {
|
get estimatedHeight(): number {
|
||||||
const cachedHeight = this.client.getCachedWidgetHeight(`image:${this.url}`);
|
const cachedHeight = this.client.getCachedWidgetHeight(`image:${this.url}`);
|
||||||
// console.log("Estimated height requested", this.url, cachedHeight);
|
|
||||||
return cachedHeight;
|
return cachedHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +114,9 @@ export function inlineImagesPlugin(client: Client) {
|
||||||
(match = /(!?\[\[)([^\]\|]+)(?:\|([^\]]+))?(\]\])/g.exec(text))
|
(match = /(!?\[\[)([^\]\|]+)(?:\|([^\]]+))?(\]\])/g.exec(text))
|
||||||
) {
|
) {
|
||||||
[/* fullMatch */, /* firstMark */ , url, alias] = match;
|
[/* fullMatch */, /* firstMark */ , url, alias] = match;
|
||||||
|
if (!isFederationPath(url)) {
|
||||||
url = "/" + url;
|
url = "/" + url;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -130,11 +133,45 @@ export function inlineImagesPlugin(client: Client) {
|
||||||
|
|
||||||
if (isLocalPath(url)) {
|
if (isLocalPath(url)) {
|
||||||
url = resolvePath(client.currentPage, decodeURI(url), true);
|
url = resolvePath(client.currentPage, decodeURI(url), true);
|
||||||
|
const pageRef = parsePageRef(url);
|
||||||
|
if (
|
||||||
|
isFederationPath(pageRef.page) ||
|
||||||
|
client.clientSystem.allKnownFiles.has(pageRef.page + ".md")
|
||||||
|
) {
|
||||||
|
// This is a page reference, let's inline the content
|
||||||
|
const codeWidgetCallback = client.clientSystem.codeWidgetHook
|
||||||
|
.codeWidgetCallbacks.get("template");
|
||||||
|
|
||||||
|
if (!codeWidgetCallback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
widgets.push(
|
||||||
|
Decoration.line({
|
||||||
|
class: "sb-fenced-code-iframe",
|
||||||
|
}).range(node.to),
|
||||||
|
);
|
||||||
|
|
||||||
|
widgets.push(
|
||||||
|
Decoration.widget({
|
||||||
|
widget: new MarkdownWidget(
|
||||||
|
node.from,
|
||||||
|
client,
|
||||||
|
`widget:${client.currentPage}:${text}`,
|
||||||
|
`{{[[${url}]]}}`,
|
||||||
|
codeWidgetCallback,
|
||||||
|
"sb-markdown-widget sb-markdown-widget-inline",
|
||||||
|
),
|
||||||
|
block: true,
|
||||||
|
}).range(node.to),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
widgets.push(
|
widgets.push(
|
||||||
Decoration.widget({
|
Decoration.widget({
|
||||||
widget: new InlineImageWidget(url, alias, dim, client),
|
widget: new InlineContentWidget(url, alias, dim, client),
|
||||||
block: true,
|
block: true,
|
||||||
}).range(node.to),
|
}).range(node.to),
|
||||||
);
|
);
|
|
@ -27,7 +27,7 @@ import {
|
||||||
import { vim } from "@replit/codemirror-vim";
|
import { vim } from "@replit/codemirror-vim";
|
||||||
import { markdown } from "@codemirror/lang-markdown";
|
import { markdown } from "@codemirror/lang-markdown";
|
||||||
import { Client } from "./client.ts";
|
import { Client } from "./client.ts";
|
||||||
import { inlineImagesPlugin } from "./cm_plugins/inline_image.ts";
|
import { inlineImagesPlugin } from "./cm_plugins/inline_content.ts";
|
||||||
import { cleanModePlugins } from "./cm_plugins/clean.ts";
|
import { cleanModePlugins } from "./cm_plugins/clean.ts";
|
||||||
import { lineWrapper } from "./cm_plugins/line_wrapper.ts";
|
import { lineWrapper } from "./cm_plugins/line_wrapper.ts";
|
||||||
import { smartQuoteKeymap } from "./cm_plugins/smart_quotes.ts";
|
import { smartQuoteKeymap } from "./cm_plugins/smart_quotes.ts";
|
||||||
|
|
|
@ -581,6 +581,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sb-markdown-widget-inline {
|
||||||
|
margin: 0 0 0 0;
|
||||||
|
|
||||||
|
&:hover .button-bar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sb-fenced-code-iframe {
|
.sb-fenced-code-iframe {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,8 @@ Images can also be embedded using the [[#Linking]] syntax, but prefixed with an
|
||||||
|
|
||||||
These follow the same relative/absolute path rules as links described before.
|
These follow the same relative/absolute path rules as links described before.
|
||||||
|
|
||||||
|
## Image resizing
|
||||||
|
|
||||||
In addition, images can be _sized_ using the following syntax:
|
In addition, images can be _sized_ using the following syntax:
|
||||||
* Specifying only a width: `![Alt text|300](image.png)` or `![[image.png|300]]`
|
* Specifying only a width: `![Alt text|300](image.png)` or `![[image.png|300]]`
|
||||||
* Specifying both width and height: `![Hello|300x300](image.png)` or `![[image.png|300x300]]`
|
* Specifying both width and height: `![Hello|300x300](image.png)` or `![[image.png|300x300]]`
|
||||||
|
|
|
@ -7,6 +7,7 @@ release.
|
||||||
_These features are not yet properly released, you need to use [the edge builds](https://community.silverbullet.md/t/living-on-the-edge-builds/27) to try them._
|
_These features are not yet properly released, you need to use [the edge builds](https://community.silverbullet.md/t/living-on-the-edge-builds/27) to try them._
|
||||||
|
|
||||||
* The old **Template Picker** has now been rebranded to [[Meta Picker]] and surfaces pages in your space tagged as `#template` or `#meta`. Read more about this in [[Meta Pages]].
|
* The old **Template Picker** has now been rebranded to [[Meta Picker]] and surfaces pages in your space tagged as `#template` or `#meta`. Read more about this in [[Meta Pages]].
|
||||||
|
* [[Transclusion]] has now been implementing, allowing inline embeddings of other pages as well as images (by onespaceman) using the convenient `![[link]]` syntax.
|
||||||
* For new spaces, the default [[SETTINGS]] page is now tagged with `#meta`, which means it will only appear in the [[Meta Picker]]. There is also a new {[Navigate: Open SETTINGS]} command (bound to `Ctrl-,` and `Cmd-,`).
|
* For new spaces, the default [[SETTINGS]] page is now tagged with `#meta`, which means it will only appear in the [[Meta Picker]]. There is also a new {[Navigate: Open SETTINGS]} command (bound to `Ctrl-,` and `Cmd-,`).
|
||||||
* Attachments are now indexed, and smartly moved when pages are renamed (by onespaceman)
|
* Attachments are now indexed, and smartly moved when pages are renamed (by onespaceman)
|
||||||
* Images can now be resized: [[Attachments#Embedding]] (initial work done by [Florent](https://github.com/silverbulletmd/silverbullet/pull/833), later adapted by onespaceman)
|
* Images can now be resized: [[Attachments#Embedding]] (initial work done by [Florent](https://github.com/silverbulletmd/silverbullet/pull/833), later adapted by onespaceman)
|
||||||
|
|
|
@ -63,9 +63,9 @@ A rendered query:
|
||||||
Page references use the `[[page name]]` syntax and evaluate to the content of the referenced page (as a string), this makes them a good candidate to be used in conjunction with [[Functions#template(text, value)]] or to simply inline another page:
|
Page references use the `[[page name]]` syntax and evaluate to the content of the referenced page (as a string), this makes them a good candidate to be used in conjunction with [[Functions#template(text, value)]] or to simply inline another page:
|
||||||
|
|
||||||
```template
|
```template
|
||||||
Including another page directly, without template rendering: {{[[internal/test page]]}}
|
Including another page directly, without template rendering: {{[[internal/test page with template]]}}
|
||||||
|
|
||||||
And rendered as a template: {{template([[internal/test page]], "actual value")}}
|
And rendered as a template: {{template([[internal/test page with template]], "actual value")}}
|
||||||
```
|
```
|
||||||
|
|
||||||
# Logical expressions
|
# Logical expressions
|
||||||
|
|
|
@ -7,6 +7,7 @@ In addition to supporting [[Markdown/Basics|markdown basics]] as standardized by
|
||||||
* [[Live Queries]]
|
* [[Live Queries]]
|
||||||
* [[Live Templates]]
|
* [[Live Templates]]
|
||||||
* [[Table of Contents]]
|
* [[Table of Contents]]
|
||||||
|
* [[Transclusion]] syntax
|
||||||
* [[Markdown/Anchors]]
|
* [[Markdown/Anchors]]
|
||||||
* [[Markdown/Admonitions]]
|
* [[Markdown/Admonitions]]
|
||||||
* Hashtags, e.g. `#mytag`.
|
* Hashtags, e.g. `#mytag`.
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
Transclusions are an extension of the [[Markdown]] syntax enabling inline embedding of content.
|
||||||
|
|
||||||
|
The general syntax is `![[path]]`. Two types of transclusions are currently supported:
|
||||||
|
|
||||||
|
# Images
|
||||||
|
Syntax: `![[path/to/image.jpg]]` see [[Attachments#Embedding]] for more details.
|
||||||
|
|
||||||
|
Image resizing is also supported:
|
||||||
|
![[Attachments#Image resizing]]
|
||||||
|
# Pages
|
||||||
|
Syntax:
|
||||||
|
* `![[page name]]` embed an entire page
|
||||||
|
* `![[page name#header]]` embed only a section (guarded by the given header)
|
||||||
|
* `![[page$anchor]]` embed a page _from_ the given anchor until the end of the page
|
||||||
|
* `![[page@pos]]` embed a page _from_ the given character position until the end of the page
|
||||||
|
|
||||||
|
For example, to embed a full page:
|
||||||
|
![[internal/test page]]
|
||||||
|
And just a header:
|
||||||
|
![[internal/test page#This is a header]]
|
|
@ -0,0 +1 @@
|
||||||
|
This is a simple test page with a placeholder: {{.}}.
|
|
@ -1 +1,4 @@
|
||||||
This is a simple test page with a placeholder: {{.}}.
|
This is a simple **test page**. Cool, no?
|
||||||
|
|
||||||
|
# This is a header
|
||||||
|
And some content underneath
|
Loading…
Reference in New Issue