From afa160d2c2e7fae44a404ed71106a52c38cdff72 Mon Sep 17 00:00:00 2001
From: Zef Hemel <zef@zef.me>
Date: Sun, 30 Jul 2023 08:56:44 +0200
Subject: [PATCH] Cleanup and federation prep

---
 common/spaces/fallback_space_primitives.ts | 30 +++++++----
 common/types.ts                            |  2 +-
 plug-api/lib/resolve.ts                    |  6 ++-
 plugs/directive/command.ts                 | 21 ++++++--
 plugs/directive/directives.ts              |  8 ---
 plugs/directive/template_directive.ts      | 59 ++++++++++++++++++++--
 web/sync_service.ts                        |  4 +-
 7 files changed, 98 insertions(+), 32 deletions(-)

diff --git a/common/spaces/fallback_space_primitives.ts b/common/spaces/fallback_space_primitives.ts
index a7d9e13..dc0f98d 100644
--- a/common/spaces/fallback_space_primitives.ts
+++ b/common/spaces/fallback_space_primitives.ts
@@ -22,15 +22,19 @@ export class FallbackSpacePrimitives implements SpacePrimitives {
     try {
       return await this.primary.readFile(name);
     } catch (e) {
-      // console.info(
-      //   `Could not read file ${name} from primary, trying fallback, primary read error:`,
-      //   e.message,
-      // );
+      if (e.message === "Not found") {
+        console.info("Reading file content from fallback for", name);
+      } else {
+        console.warn(
+          `Could not read file ${name} from primary, trying fallback, primary read error`,
+          e.message,
+        );
+      }
       try {
         const result = await this.fallback.readFile(name);
         return {
           data: result.data,
-          meta: { ...result.meta, neverSync: true },
+          meta: { ...result.meta, noSync: true },
         };
       } catch (fallbackError: any) {
         console.error("Error during readFile fallback", fallbackError.message);
@@ -42,14 +46,18 @@ export class FallbackSpacePrimitives implements SpacePrimitives {
   async getFileMeta(name: string): Promise<FileMeta> {
     try {
       return await this.primary.getFileMeta(name);
-    } catch (e) {
-      // console.info(
-      //   `Could not fetch file ${name} metadata from primary, trying fallback, primary read error`,
-      //   e.message,
-      // );
+    } catch (e: any) {
+      if (e.message === "Not found") {
+        console.info("Fetching file meta from fallback for", name);
+      } else {
+        console.warn(
+          `Could not fetch file ${name} metadata from primary, trying fallback, primary read error`,
+          e.message,
+        );
+      }
       try {
         const meta = await this.fallback.getFileMeta(name);
-        return { ...meta, neverSync: true };
+        return { ...meta, noSync: true };
       } catch (fallbackError) {
         console.error(
           "Error during getFileMeta fallback",
diff --git a/common/types.ts b/common/types.ts
index cf54e60..404e778 100644
--- a/common/types.ts
+++ b/common/types.ts
@@ -6,5 +6,5 @@ export type FileMeta = {
   contentType: string;
   size: number;
   perm: "ro" | "rw";
-  neverSync?: boolean;
+  noSync?: boolean;
 } & Record<string, any>;
diff --git a/plug-api/lib/resolve.ts b/plug-api/lib/resolve.ts
index 74bab1f..a4dfd6a 100644
--- a/plug-api/lib/resolve.ts
+++ b/plug-api/lib/resolve.ts
@@ -3,7 +3,7 @@ export function resolvePath(
   pathToResolve: string,
   fullUrl = false,
 ): string {
-  if (currentPage.startsWith("!") && !pathToResolve.startsWith("!")) {
+  if (isFederationPath(currentPage) && !isFederationPath(pathToResolve)) {
     let domainPart = currentPage.split("/")[0];
     if (fullUrl) {
       domainPart = domainPart.substring(1);
@@ -18,3 +18,7 @@ export function resolvePath(
     return pathToResolve;
   }
 }
+
+export function isFederationPath(path: string) {
+  return path.startsWith("!");
+}
diff --git a/plugs/directive/command.ts b/plugs/directive/command.ts
index 45e3de1..c76c009 100644
--- a/plugs/directive/command.ts
+++ b/plugs/directive/command.ts
@@ -1,4 +1,4 @@
-import { editor, markdown, space } from "$sb/silverbullet-syscall/mod.ts";
+import { editor, markdown, space, sync } from "$sb/silverbullet-syscall/mod.ts";
 import {
   removeParentPointers,
   renderToText,
@@ -7,15 +7,29 @@ import {
 import { renderDirectives } from "./directives.ts";
 import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
 import { PageMeta } from "../../web/types.ts";
+import { isFederationPath } from "$sb/lib/resolve.ts";
 
 export async function updateDirectivesOnPageCommand() {
   // If `arg` is a string, it's triggered automatically via an event, not explicitly via a command
-  const pageMeta = await space.getPageMeta(await editor.getCurrentPage());
+  const currentPage = await editor.getCurrentPage();
+  const pageMeta = await space.getPageMeta(currentPage);
   const text = await editor.getText();
   const tree = await markdown.parseMarkdown(text);
   const metaData = await extractFrontmatter(tree, ["$disableDirectives"]);
+
+  if (isFederationPath(currentPage)) {
+    console.info("Current page is a federation page, not updating directives.");
+  }
+
   if (metaData.$disableDirectives) {
-    // Not updating, directives disabled
+    console.info("Directives disabled in page meta, not updating them.");
+    return;
+  }
+
+  if (!(await sync.hasInitialSyncCompleted())) {
+    console.info(
+      "Initial sync hasn't completed yet, not updating directives.",
+    );
     return;
   }
 
@@ -96,7 +110,6 @@ export async function updateDirectivesOnPageCommand() {
   }
 }
 
-// Pure server driven implementation of directive updating
 export async function updateDirectives(
   pageMeta: PageMeta,
   text: string,
diff --git a/plugs/directive/directives.ts b/plugs/directive/directives.ts
index 8633bd8..4dd3b8e 100644
--- a/plugs/directive/directives.ts
+++ b/plugs/directive/directives.ts
@@ -35,14 +35,6 @@ export async function directiveDispatcher(
   const directiveStartText = renderToText(directiveStart).trim();
   const directiveEndText = renderToText(directiveEnd).trim();
 
-  if (!(await sync.hasInitialSyncCompleted())) {
-    console.info(
-      "Initial sync hasn't completed yet, not updating directives.",
-    );
-    // Render the query directive as-is
-    return renderToText(directiveTree);
-  }
-
   if (directiveStart.children!.length === 1) {
     // Everything not #query
     const match = directiveStartRegex.exec(directiveStart.children![0].text!);
diff --git a/plugs/directive/template_directive.ts b/plugs/directive/template_directive.ts
index ef3fecc..58c9284 100644
--- a/plugs/directive/template_directive.ts
+++ b/plugs/directive/template_directive.ts
@@ -1,5 +1,10 @@
 import { queryRegex } from "$sb/lib/query.ts";
-import { ParseTree, renderToText } from "$sb/lib/tree.ts";
+import {
+  findNodeOfType,
+  ParseTree,
+  renderToText,
+  traverseTree,
+} from "$sb/lib/tree.ts";
 import { markdown, space } from "$sb/silverbullet-syscall/mod.ts";
 import Handlebars from "handlebars";
 
@@ -9,6 +14,7 @@ import { directiveRegex } from "./directives.ts";
 import { updateDirectives } from "./command.ts";
 import { buildHandebarOptions } from "./util.ts";
 import { PageMeta } from "../../web/types.ts";
+import { resolvePath } from "$sb/lib/resolve.ts";
 
 const templateRegex = /\[\[([^\]]+)\]\]\s*(.*)\s*/;
 
@@ -24,7 +30,7 @@ export async function templateDirectiveRenderer(
   if (!match) {
     throw new Error(`Invalid template directive: ${arg}`);
   }
-  const template = match[1];
+  let templatePath = match[1];
   const args = match[2];
   let parsedArgs = {};
   if (args) {
@@ -39,20 +45,29 @@ export async function templateDirectiveRenderer(
     }
   }
   let templateText = "";
-  if (template.startsWith("http://") || template.startsWith("https://")) {
+  if (
+    templatePath.startsWith("http://") || templatePath.startsWith("https://")
+  ) {
     try {
-      const req = await fetch(template);
+      const req = await fetch(templatePath);
       templateText = await req.text();
     } catch (e: any) {
       templateText = `ERROR: ${e.message}`;
     }
   } else {
-    templateText = await space.readPage(template);
+    templatePath = resolvePath(pageMeta.name, templatePath);
+    templateText = await space.readPage(templatePath);
   }
   const tree = await markdown.parseMarkdown(templateText);
   await extractFrontmatter(tree, [], true); // Remove entire frontmatter section, if any
+
+  // Resolve paths in the template
+  rewritePageRefs(tree, templatePath);
+
   let newBody = renderToText(tree);
 
+  // console.log("Rewritten template:", newBody);
+
   // if it's a template injection (not a literal "include")
   if (directive === "use") {
     const templateFn = Handlebars.compile(
@@ -67,6 +82,40 @@ export async function templateDirectiveRenderer(
   return newBody.trim();
 }
 
+function rewritePageRefs(tree: ParseTree, templatePath: string) {
+  traverseTree(tree, (n): boolean => {
+    if (n.type === "DirectiveStart") {
+      const pageRef = findNodeOfType(n, "PageRef")!;
+      if (pageRef) {
+        const pageRefName = pageRef.children![0].text!.slice(2, -2);
+        pageRef.children![0].text = `[[${
+          resolvePath(templatePath, pageRefName)
+        }]]`;
+      }
+      const directiveText = n.children![0].text;
+      // #use or #import
+      if (directiveText) {
+        const match = /\[\[(.+)\]\]/.exec(directiveText);
+        if (match) {
+          const pageRefName = match[1];
+          n.children![0].text = directiveText.replace(
+            match[0],
+            `[[${resolvePath(templatePath, pageRefName)}]]`,
+          );
+        }
+      }
+
+      return true;
+    }
+    if (n.type === "WikiLinkPage") {
+      n.children![0].text = resolvePath(templatePath, n.children![0].text!);
+      return true;
+    }
+
+    return false;
+  });
+}
+
 export function cleanTemplateInstantiations(text: string) {
   return text.replaceAll(directiveRegex, (
     _fullMatch,
diff --git a/web/sync_service.ts b/web/sync_service.ts
index e5e9a9c..1dea4be 100644
--- a/web/sync_service.ts
+++ b/web/sync_service.ts
@@ -192,9 +192,9 @@ export class SyncService {
       let remoteHash: number | undefined;
       try {
         const localMeta = await this.localSpacePrimitives.getFileMeta(name);
-        if (localMeta.neverSync) {
+        if (localMeta.noSync) {
           console.info(
-            "File marked as neverSync, skipping sync in this cycle",
+            "File marked as no sync, skipping sync in this cycle",
             name,
           );
           await this.registerSyncStop();