parent
fefda09c2b
commit
11967b82a6
|
@ -21,6 +21,9 @@ Deno.test("Page utility functions", () => {
|
||||||
pos: 1,
|
pos: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Meta page
|
||||||
|
assertEquals(parsePageRef("^foo"), { page: "foo", meta: true });
|
||||||
|
|
||||||
// Edge cases
|
// Edge cases
|
||||||
assertEquals(parsePageRef(""), { page: "" });
|
assertEquals(parsePageRef(""), { page: "" });
|
||||||
assertEquals(parsePageRef("user@domain.com"), { page: "user@domain.com" });
|
assertEquals(parsePageRef("user@domain.com"), { page: "user@domain.com" });
|
||||||
|
|
|
@ -23,6 +23,7 @@ export type PageRef = {
|
||||||
pos?: number;
|
pos?: number;
|
||||||
anchor?: string;
|
anchor?: string;
|
||||||
header?: string;
|
header?: string;
|
||||||
|
meta?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const posRegex = /@(\d+)$/;
|
const posRegex = /@(\d+)$/;
|
||||||
|
@ -36,6 +37,11 @@ export function parsePageRef(name: string): PageRef {
|
||||||
name = name.slice(2, -2);
|
name = name.slice(2, -2);
|
||||||
}
|
}
|
||||||
const pageRef: PageRef = { page: name };
|
const pageRef: PageRef = { page: name };
|
||||||
|
if (pageRef.page.startsWith("^")) {
|
||||||
|
// A carrot prefix means we're looking for a meta page, but that doesn't matter for most use cases
|
||||||
|
pageRef.page = pageRef.page.slice(1);
|
||||||
|
pageRef.meta = true;
|
||||||
|
}
|
||||||
const posMatch = pageRef.page.match(posRegex);
|
const posMatch = pageRef.page.match(posRegex);
|
||||||
if (posMatch) {
|
if (posMatch) {
|
||||||
pageRef.pos = parseInt(posMatch[1]);
|
pageRef.pos = parseInt(posMatch[1]);
|
||||||
|
|
|
@ -3,11 +3,22 @@ import {
|
||||||
CompleteEvent,
|
CompleteEvent,
|
||||||
FileMeta,
|
FileMeta,
|
||||||
PageMeta,
|
PageMeta,
|
||||||
|
QueryExpression,
|
||||||
} from "$sb/types.ts";
|
} from "$sb/types.ts";
|
||||||
import { listFilesCached } from "../federation/federation.ts";
|
import { listFilesCached } from "../federation/federation.ts";
|
||||||
import { queryObjects } from "../index/plug_api.ts";
|
import { queryObjects } from "../index/plug_api.ts";
|
||||||
import { folderName } from "$sb/lib/resolve.ts";
|
import { folderName } from "$sb/lib/resolve.ts";
|
||||||
import { decoration } from "$sb/syscalls.ts";
|
import { decoration } from "$sb/syscalls.ts";
|
||||||
|
|
||||||
|
// A meta page is a page tagged with either #template or #meta
|
||||||
|
const isMetaPageFilter: QueryExpression = ["or", ["=", ["attr", "tags"], [
|
||||||
|
"string",
|
||||||
|
"template",
|
||||||
|
]], ["=", [
|
||||||
|
"attr",
|
||||||
|
"tags",
|
||||||
|
], ["string", "meta"]]];
|
||||||
|
|
||||||
// Completion
|
// Completion
|
||||||
export async function pageComplete(completeEvent: CompleteEvent) {
|
export async function pageComplete(completeEvent: CompleteEvent) {
|
||||||
// Try to match [[wikilink]]
|
// Try to match [[wikilink]]
|
||||||
|
@ -22,16 +33,29 @@ export async function pageComplete(completeEvent: CompleteEvent) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prefix = match[1];
|
||||||
|
|
||||||
let allPages: (PageMeta | AttachmentMeta)[] = [];
|
let allPages: (PageMeta | AttachmentMeta)[] = [];
|
||||||
|
|
||||||
if (
|
if (prefix.startsWith("^")) {
|
||||||
|
// A carrot prefix means we're looking for a meta page
|
||||||
|
allPages = await queryObjects<PageMeta>("page", {
|
||||||
|
filter: isMetaPageFilter,
|
||||||
|
}, 5);
|
||||||
|
// Let's prefix the names with a caret to make them match
|
||||||
|
allPages = allPages.map((page) => ({
|
||||||
|
...page,
|
||||||
|
name: "^" + page.name,
|
||||||
|
}));
|
||||||
|
} // Let's try to be smart about the types of completions we're offering based on the context
|
||||||
|
else if (
|
||||||
completeEvent.parentNodes.find((node) => node.startsWith("FencedCode")) &&
|
completeEvent.parentNodes.find((node) => node.startsWith("FencedCode")) &&
|
||||||
// either a render [[bla]] clause
|
// either a render [[bla]] clause
|
||||||
/(render\s+|template\()\[\[/.test(
|
/(render\s+|template\()\[\[/.test(
|
||||||
completeEvent.linePrefix,
|
completeEvent.linePrefix,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// We're in a template context, let's only complete templates
|
// We're quite certainly in a template context, let's only complete templates
|
||||||
allPages = await queryObjects<PageMeta>("template", {}, 5);
|
allPages = await queryObjects<PageMeta>("template", {}, 5);
|
||||||
} else if (
|
} else if (
|
||||||
completeEvent.parentNodes.find((node) =>
|
completeEvent.parentNodes.find((node) =>
|
||||||
|
@ -44,10 +68,7 @@ export async function pageComplete(completeEvent: CompleteEvent) {
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, just complete non-meta pages
|
// Otherwise, just complete non-meta pages
|
||||||
allPages = await queryObjects<PageMeta>("page", {
|
allPages = await queryObjects<PageMeta>("page", {
|
||||||
filter: ["and", ["!=", ["attr", "tags"], ["string", "template"]], ["!=", [
|
filter: ["not", isMetaPageFilter],
|
||||||
"attr",
|
|
||||||
"tags",
|
|
||||||
], ["string", "meta"]]],
|
|
||||||
}, 5);
|
}, 5);
|
||||||
// and attachments
|
// and attachments
|
||||||
allPages = allPages.concat(
|
allPages = allPages.concat(
|
||||||
|
@ -55,34 +76,29 @@ export async function pageComplete(completeEvent: CompleteEvent) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefix = match[1];
|
|
||||||
if (prefix.startsWith("!")) {
|
if (prefix.startsWith("!")) {
|
||||||
// Federation prefix, let's first see if we're matching anything from federation that is locally synced
|
// Federation!
|
||||||
const prefixMatches = allPages.filter((pageMeta) =>
|
// Let's see if this URI is complete enough to try to fetch index.json
|
||||||
pageMeta.name.startsWith(prefix)
|
if (prefix.includes("/")) {
|
||||||
);
|
// Yep
|
||||||
if (prefixMatches.length === 0) {
|
const domain = prefix.split("/")[0];
|
||||||
// Ok, nothing synced in via federation, let's see if this URI is complete enough to try to fetch index.json
|
// Cached listing
|
||||||
if (prefix.includes("/")) {
|
const federationPages = (await listFilesCached(domain)).filter((fm) =>
|
||||||
// Yep
|
fm.name.endsWith(".md")
|
||||||
const domain = prefix.split("/")[0];
|
).map(fileMetaToPageMeta);
|
||||||
// Cached listing
|
if (federationPages.length > 0) {
|
||||||
const federationPages = (await listFilesCached(domain)).filter((fm) =>
|
allPages = allPages.concat(federationPages);
|
||||||
fm.name.endsWith(".md")
|
|
||||||
).map(fileMetaToPageMeta);
|
|
||||||
if (federationPages.length > 0) {
|
|
||||||
allPages = allPages.concat(federationPages);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const folder = folderName(completeEvent.pageName);
|
const folder = folderName(completeEvent.pageName);
|
||||||
|
|
||||||
|
// Decorate the pages
|
||||||
allPages = await decoration.applyDecorationsToPages(allPages as PageMeta[]);
|
allPages = await decoration.applyDecorationsToPages(allPages as PageMeta[]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from: completeEvent.pos - match[1].length,
|
from: completeEvent.pos - prefix.length,
|
||||||
options: allPages.map((pageMeta) => {
|
options: allPages.map((pageMeta) => {
|
||||||
const completions: any[] = [];
|
const completions: any[] = [];
|
||||||
let namePrefix = "";
|
let namePrefix = "";
|
||||||
|
@ -90,10 +106,11 @@ export async function pageComplete(completeEvent: CompleteEvent) {
|
||||||
namePrefix = pageMeta.pageDecoration?.prefix;
|
namePrefix = pageMeta.pageDecoration?.prefix;
|
||||||
}
|
}
|
||||||
if (isWikilink) {
|
if (isWikilink) {
|
||||||
|
// A [[wikilink]]
|
||||||
if (pageMeta.displayName) {
|
if (pageMeta.displayName) {
|
||||||
const decoratedName = namePrefix + pageMeta.displayName;
|
const decoratedName = namePrefix + pageMeta.displayName;
|
||||||
completions.push({
|
completions.push({
|
||||||
label: `${pageMeta.displayName}`,
|
label: pageMeta.displayName,
|
||||||
displayLabel: decoratedName,
|
displayLabel: decoratedName,
|
||||||
boost: new Date(pageMeta.lastModified).getTime(),
|
boost: new Date(pageMeta.lastModified).getTime(),
|
||||||
apply: pageMeta.tag === "template"
|
apply: pageMeta.tag === "template"
|
||||||
|
@ -107,7 +124,7 @@ export async function pageComplete(completeEvent: CompleteEvent) {
|
||||||
for (const alias of pageMeta.aliases) {
|
for (const alias of pageMeta.aliases) {
|
||||||
const decoratedName = namePrefix + alias;
|
const decoratedName = namePrefix + alias;
|
||||||
completions.push({
|
completions.push({
|
||||||
label: `${alias}`,
|
label: alias,
|
||||||
displayLabel: decoratedName,
|
displayLabel: decoratedName,
|
||||||
boost: new Date(pageMeta.lastModified).getTime(),
|
boost: new Date(pageMeta.lastModified).getTime(),
|
||||||
apply: pageMeta.tag === "template"
|
apply: pageMeta.tag === "template"
|
||||||
|
@ -120,12 +137,13 @@ export async function pageComplete(completeEvent: CompleteEvent) {
|
||||||
}
|
}
|
||||||
const decoratedName = namePrefix + pageMeta.name;
|
const decoratedName = namePrefix + pageMeta.name;
|
||||||
completions.push({
|
completions.push({
|
||||||
label: `${pageMeta.name}`,
|
label: pageMeta.name,
|
||||||
displayLabel: decoratedName,
|
displayLabel: decoratedName,
|
||||||
boost: new Date(pageMeta.lastModified).getTime(),
|
boost: new Date(pageMeta.lastModified).getTime(),
|
||||||
type: "page",
|
type: "page",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
// A markdown link []()
|
||||||
let labelText = pageMeta.name;
|
let labelText = pageMeta.name;
|
||||||
let boost = new Date(pageMeta.lastModified).getTime();
|
let boost = new Date(pageMeta.lastModified).getTime();
|
||||||
// Relative path if in the same folder or a subfolder
|
// Relative path if in the same folder or a subfolder
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { readSetting } from "$sb/lib/settings_page.ts";
|
|
||||||
|
|
||||||
type FederationConfig = {
|
|
||||||
uri: string;
|
|
||||||
perm?: "ro" | "rw";
|
|
||||||
// TODO: alias?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
let federationConfigs: FederationConfig[] = [];
|
|
||||||
let lastFederationUrlFetch = 0;
|
|
||||||
|
|
||||||
export async function readFederationConfigs(): Promise<FederationConfig[]> {
|
|
||||||
// Update at most every 5 seconds
|
|
||||||
if (Date.now() > lastFederationUrlFetch + 5000) {
|
|
||||||
federationConfigs = await readSetting("federate", []);
|
|
||||||
if (!Array.isArray(federationConfigs)) {
|
|
||||||
console.error("'federate' setting should be an array of objects");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
// Normalize URIs
|
|
||||||
for (const config of federationConfigs) {
|
|
||||||
if (!config.uri) {
|
|
||||||
console.error(
|
|
||||||
"'federate' setting should be an array of objects with at least an 'uri' property",
|
|
||||||
config,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!config.uri.startsWith("!")) {
|
|
||||||
config.uri = `!${config.uri}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastFederationUrlFetch = Date.now();
|
|
||||||
}
|
|
||||||
return federationConfigs;
|
|
||||||
}
|
|
|
@ -2,12 +2,6 @@ name: federation
|
||||||
requiredPermissions:
|
requiredPermissions:
|
||||||
- fetch
|
- fetch
|
||||||
functions:
|
functions:
|
||||||
listFiles:
|
|
||||||
path: ./federation.ts:listFiles
|
|
||||||
env: server
|
|
||||||
pageNamespace:
|
|
||||||
pattern: "!.+"
|
|
||||||
operation: listFiles
|
|
||||||
readFile:
|
readFile:
|
||||||
path: ./federation.ts:readFile
|
path: ./federation.ts:readFile
|
||||||
pageNamespace:
|
pageNamespace:
|
||||||
|
|
|
@ -1,31 +1,20 @@
|
||||||
import "$sb/lib/native_fetch.ts";
|
import "$sb/lib/native_fetch.ts";
|
||||||
import { federatedPathToUrl } from "$sb/lib/resolve.ts";
|
import { federatedPathToUrl } from "$sb/lib/resolve.ts";
|
||||||
import { readFederationConfigs } from "./config.ts";
|
|
||||||
import { datastore } from "$sb/syscalls.ts";
|
import { datastore } from "$sb/syscalls.ts";
|
||||||
import type { FileMeta } from "../../plug-api/types.ts";
|
import type { FileMeta } from "../../plug-api/types.ts";
|
||||||
import { wildcardPathToRegex } from "./util.ts";
|
import { wildcardPathToRegex } from "./util.ts";
|
||||||
|
|
||||||
async function responseToFileMeta(
|
function responseToFileMeta(
|
||||||
r: Response,
|
r: Response,
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<FileMeta> {
|
): FileMeta {
|
||||||
const federationConfigs = await readFederationConfigs();
|
|
||||||
|
|
||||||
// Default permission is "ro" unless explicitly set otherwise
|
|
||||||
let perm: "ro" | "rw" = "ro";
|
|
||||||
const federationConfig = federationConfigs.find((config) =>
|
|
||||||
name.startsWith(config.uri)
|
|
||||||
);
|
|
||||||
if (federationConfig?.perm) {
|
|
||||||
perm = federationConfig.perm;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
name: name,
|
name: name,
|
||||||
size: r.headers.get("Content-length")
|
size: r.headers.get("Content-length")
|
||||||
? +r.headers.get("Content-length")!
|
? +r.headers.get("Content-length")!
|
||||||
: 0,
|
: 0,
|
||||||
contentType: r.headers.get("Content-type")!,
|
contentType: r.headers.get("Content-type")!,
|
||||||
perm,
|
perm: "ro",
|
||||||
created: +(r.headers.get("X-Created") || "0"),
|
created: +(r.headers.get("X-Created") || "0"),
|
||||||
lastModified: +(r.headers.get("X-Last-Modified") || "0"),
|
lastModified: +(r.headers.get("X-Last-Modified") || "0"),
|
||||||
};
|
};
|
||||||
|
@ -40,23 +29,6 @@ type FileListingCacheEntry = {
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function listFiles(): Promise<FileMeta[]> {
|
|
||||||
let fileMetas: FileMeta[] = [];
|
|
||||||
// Fetch them all in parallel
|
|
||||||
try {
|
|
||||||
await Promise.all((await readFederationConfigs()).map(async (config) => {
|
|
||||||
const items = await listFilesCached(config.uri);
|
|
||||||
fileMetas = fileMetas.concat(items);
|
|
||||||
}));
|
|
||||||
|
|
||||||
// console.log("All of em: ", fileMetas);
|
|
||||||
return fileMetas;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Error listing federation files", e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listFilesCached(
|
export async function listFilesCached(
|
||||||
uri: string,
|
uri: string,
|
||||||
supportWildcards = false,
|
supportWildcards = false,
|
||||||
|
@ -145,7 +117,7 @@ export async function readFile(
|
||||||
if (r.status === 503) {
|
if (r.status === 503) {
|
||||||
throw new Error("Offline");
|
throw new Error("Offline");
|
||||||
}
|
}
|
||||||
const fileMeta = await responseToFileMeta(r, name);
|
const fileMeta = responseToFileMeta(r, name);
|
||||||
if (r.status === 404) {
|
if (r.status === 404) {
|
||||||
throw Error("Not found");
|
throw Error("Not found");
|
||||||
}
|
}
|
||||||
|
@ -223,7 +195,7 @@ export async function getFileMeta(name: string): Promise<FileMeta> {
|
||||||
if (r.status === 503) {
|
if (r.status === 503) {
|
||||||
throw new Error("Offline");
|
throw new Error("Offline");
|
||||||
}
|
}
|
||||||
const fileMeta = await responseToFileMeta(r, name);
|
const fileMeta = responseToFileMeta(r, name);
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
throw new Error("Not found");
|
throw new Error("Not found");
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,9 +69,13 @@ export function cleanWikiLinkPlugin(client: Client) {
|
||||||
const pageMeta = client.ui.viewState.allPages.find((p) =>
|
const pageMeta = client.ui.viewState.allPages.find((p) =>
|
||||||
p.name == url
|
p.name == url
|
||||||
);
|
);
|
||||||
|
let cleanLinkText = url.includes("/") ? url.split("/").pop()! : url;
|
||||||
|
if (cleanLinkText.startsWith("^")) {
|
||||||
|
// Hide the ^ prefix
|
||||||
|
cleanLinkText = cleanLinkText.slice(1);
|
||||||
|
}
|
||||||
const linkText = alias ||
|
const linkText = alias ||
|
||||||
(pageMeta?.pageDecoration?.prefix ?? "") +
|
((pageMeta?.pageDecoration?.prefix ?? "") + cleanLinkText);
|
||||||
(url.includes("/") ? url.split("/").pop()! : url);
|
|
||||||
|
|
||||||
// And replace it with a widget
|
// And replace it with a widget
|
||||||
widgets.push(
|
widgets.push(
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { FilterList } from "./filter.tsx";
|
||||||
import { FilterOption } from "$lib/web.ts";
|
import { FilterOption } from "$lib/web.ts";
|
||||||
import { CompletionContext, CompletionResult } from "@codemirror/autocomplete";
|
import { CompletionContext, CompletionResult } from "@codemirror/autocomplete";
|
||||||
import { PageMeta } from "../../plug-api/types.ts";
|
import { PageMeta } from "../../plug-api/types.ts";
|
||||||
import { isFederationPath } from "$sb/lib/resolve.ts";
|
|
||||||
import { tagRegex as mdTagRegex } from "$common/markdown_parser/parser.ts";
|
import { tagRegex as mdTagRegex } from "$common/markdown_parser/parser.ts";
|
||||||
|
|
||||||
const tagRegex = new RegExp(mdTagRegex.source, "g");
|
const tagRegex = new RegExp(mdTagRegex.source, "g");
|
||||||
|
@ -10,6 +9,7 @@ const tagRegex = new RegExp(mdTagRegex.source, "g");
|
||||||
export function PageNavigator({
|
export function PageNavigator({
|
||||||
allPages,
|
allPages,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
|
onModeSwitch,
|
||||||
completer,
|
completer,
|
||||||
vimMode,
|
vimMode,
|
||||||
mode,
|
mode,
|
||||||
|
@ -21,6 +21,7 @@ export function PageNavigator({
|
||||||
darkMode: boolean;
|
darkMode: boolean;
|
||||||
mode: "page" | "meta";
|
mode: "page" | "meta";
|
||||||
onNavigate: (page: string | undefined) => void;
|
onNavigate: (page: string | undefined) => void;
|
||||||
|
onModeSwitch: (mode: "page" | "meta") => void;
|
||||||
completer: (context: CompletionContext) => Promise<CompletionResult | null>;
|
completer: (context: CompletionContext) => Promise<CompletionResult | null>;
|
||||||
currentPage?: string;
|
currentPage?: string;
|
||||||
}) {
|
}) {
|
||||||
|
@ -41,10 +42,6 @@ export function PageNavigator({
|
||||||
// ... then we put it all the way to the end
|
// ... then we put it all the way to the end
|
||||||
orderId = Infinity;
|
orderId = Infinity;
|
||||||
}
|
}
|
||||||
// And deprioritize federated pages too
|
|
||||||
if (isFederationPath(pageMeta.name)) {
|
|
||||||
orderId = Math.round(orderId / 10); // Just 10x lower the timestamp to push them down, should work
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === "page") {
|
if (mode === "page") {
|
||||||
// Special behavior for regular pages
|
// Special behavior for regular pages
|
||||||
|
@ -101,6 +98,11 @@ export function PageNavigator({
|
||||||
phrase = phrase.replaceAll(tagRegex, "").trim();
|
phrase = phrase.replaceAll(tagRegex, "").trim();
|
||||||
return phrase;
|
return phrase;
|
||||||
}}
|
}}
|
||||||
|
onKeyPress={(key, text) => {
|
||||||
|
if (mode === "page" && key === "^" && text === "^") {
|
||||||
|
onModeSwitch("meta");
|
||||||
|
}
|
||||||
|
}}
|
||||||
preFilter={(options, phrase) => {
|
preFilter={(options, phrase) => {
|
||||||
if (mode === "page") {
|
if (mode === "page") {
|
||||||
const allTags = phrase.match(tagRegex);
|
const allTags = phrase.match(tagRegex);
|
||||||
|
|
|
@ -115,6 +115,12 @@ export class MainUI {
|
||||||
completer={client.miniEditorComplete.bind(client)}
|
completer={client.miniEditorComplete.bind(client)}
|
||||||
vimMode={viewState.uiOptions.vimMode}
|
vimMode={viewState.uiOptions.vimMode}
|
||||||
darkMode={viewState.uiOptions.darkMode}
|
darkMode={viewState.uiOptions.darkMode}
|
||||||
|
onModeSwitch={(mode) => {
|
||||||
|
dispatch({ type: "stop-navigate" });
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch({ type: "start-navigate", mode });
|
||||||
|
});
|
||||||
|
}}
|
||||||
onNavigate={(page) => {
|
onNavigate={(page) => {
|
||||||
dispatch({ type: "stop-navigate" });
|
dispatch({ type: "stop-navigate" });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
@ -9,8 +9,10 @@ _These features are not yet properly released, you need to use [the edge builds]
|
||||||
* [[Page Decorations]] are here (initial implementation by [Deepak Narayan](https://github.com/silverbulletmd/silverbullet/pull/940), later refined by Zef)
|
* [[Page Decorations]] are here (initial implementation by [Deepak Narayan](https://github.com/silverbulletmd/silverbullet/pull/940), later refined by Zef)
|
||||||
* New type of [[Shortcuts|shortcut]]: `slashCommand`
|
* New type of [[Shortcuts|shortcut]]: `slashCommand`
|
||||||
* Naming is hard. Renamed the `source` attribute of [[Libraries]] to `import`. egacy references to `source` will keep working.
|
* Naming is hard. Renamed the `source` attribute of [[Libraries]] to `import`. egacy references to `source` will keep working.
|
||||||
|
* Added support for [[Links#Caret page links]] making it slightly more convenient to link to [[Meta Pages]]
|
||||||
* **Fix:** very large spaces would let the server blow up when saving snapshots. This is now fixed.
|
* **Fix:** very large spaces would let the server blow up when saving snapshots. This is now fixed.
|
||||||
* **Fix:** Conflict copies could no longer be edited, whoops (initial fix by [Semyon Novikov](https://github.com/silverbulletmd/silverbullet/pull/939), later refined by Zef)
|
* **Fix:** Conflict copies could no longer be edited, whoops (initial fix by [Semyon Novikov](https://github.com/silverbulletmd/silverbullet/pull/939), later refined by Zef)
|
||||||
|
* **Fix**: `silverbullet upgrade` should now work again (or at least on the next upgrade)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,13 @@ You can create three types of links in SilverBullet:
|
||||||
* Internal links using the `[[page name]]` syntax
|
* Internal links using the `[[page name]]` syntax
|
||||||
|
|
||||||
# Internal link format
|
# Internal link format
|
||||||
Internal links have different formats:
|
Internal links can have various formats:
|
||||||
|
|
||||||
* `[[CHANGELOG]]`: a simple link to another page that appears like this: [[CHANGELOG]].
|
* `[[CHANGELOG]]`: a simple link to another page that appears like this: [[CHANGELOG]].
|
||||||
* `[[CHANGELOG|The Change Log]]`: a link with an alias that appears like this: [[CHANGELOG|The Change Log]].
|
* `[[CHANGELOG|The Change Log]]`: a link with an alias that appears like this: [[CHANGELOG|The Change Log]].
|
||||||
* `[[CHANGELOG$edge]]`: a link referencing a particular [[Markdown/Anchors|anchor]]: [[CHANGELOG$edge]].
|
* `[[CHANGELOG$edge]]`: a link referencing a particular [[Markdown/Anchors|anchor]]: [[CHANGELOG$edge]]. When the page name is omitted, the anchor is expected to be local to the current page.
|
||||||
* `[[CHANGELOG#Edge]]`: a link referencing a particular header: [[CHANGELOG#Edge]]
|
* `[[CHANGELOG#Edge]]`: a link referencing a particular header: [[CHANGELOG#Edge]]. When the page name is omitted, the header is expected to be local to the current page.
|
||||||
* `[[CHANGELOG@1234]]`: a link referencing a particular position in a page (characters from the start of the document). This notation is generally automatically generated through templates.
|
* `[[CHANGELOG@1234]]`: a link referencing a particular position in a page (characters from the start of the document). This notation is generally automatically generated through templates.
|
||||||
|
|
||||||
|
# Caret page links
|
||||||
|
[[Meta Pages]] are excluded from link auto complete in many contexts. However, you may still want to reference a meta page outside of a “meta context.” To make it easier to reference, you can use the caret syntax: `[[^SETTINGS]]`. Semantically this has the same meaning as `[[SETTINGS]]`. The only difference is that auto complete will _only_ complete meta pages.
|
||||||
|
|
|
@ -7,4 +7,5 @@ The most obvious example is [[SETTINGS]], which is not really a page that you ca
|
||||||
# How are meta pages identified?
|
# How are meta pages identified?
|
||||||
Meta pages at a technical level are [[Pages]] like any other, the only technical difference is that they are either tagged with `#template` or `#meta`. This is picked up by the [[Page Picker]] and [[Meta Picker]].
|
Meta pages at a technical level are [[Pages]] like any other, the only technical difference is that they are either tagged with `#template` or `#meta`. This is picked up by the [[Page Picker]] and [[Meta Picker]].
|
||||||
|
|
||||||
That’s it.
|
# How do you link to meta pages?
|
||||||
|
You can link to a meta page like any other page, that is using the `[[page name]]` syntax. However, in the context of regular content, meta pages will not appear in auto complete. To get auto completion for meta pages, you can use the `[[^page name]]` caret syntax. More information: [[Links#Caret page links]].
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
There are a few rules regarding page names:
|
There are a few rules regarding page names:
|
||||||
|
|
||||||
* Page names cannot be empty
|
* Page names cannot be empty
|
||||||
* Page names cannot start with a `.`
|
* Page names cannot start with a `.` nor `^`
|
||||||
* Page names cannot `@` or `$`, due to ambiguity with referencing specific positions or anchors inside a page
|
* Page names cannot contain `@` or `$`, due to ambiguity with referencing specific positions or anchors inside a page
|
||||||
* Page names should not contain `!`
|
* Page names should not contain `!`
|
||||||
* Page names cannot end with a _file extension_ containing just letters. That is, a page name like `test.md` is not allowed, whereas `test.123` would be.
|
* Page names cannot end with a _file extension_ containing just letters. That is, a page name like `test.md` is not allowed, whereas `test.123` would be.
|
Loading…
Reference in New Issue