diff --git a/package.json b/package.json index cb77cb5..2e63728 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@craftamap/esbuild-plugin-html": "^0.8.0", "@sveltejs/vite-plugin-svelte": "^3.1.2", "@tsconfig/svelte": "^5.0.4", + "@types/browser-util-inspect": "^0.2.4", "@types/child-process-promise": "^2.2.6", "@types/express": "^5.0.0", "@types/mime-db": "^1.43.5", @@ -61,6 +62,7 @@ "base32-decode": "^1.0.0", "base32-encode": "^2.0.0", "base85": "^3.1.0", + "browser-util-inspect": "^0.2.0", "buffer": "^6.0.3", "builtin-modules": "^4.0.0", "cbor-x": "^1.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e0f4ae..9b564a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: base85: specifier: ^3.1.0 version: 3.1.0 + browser-util-inspect: + specifier: ^0.2.0 + version: 0.2.0 buffer: specifier: ^6.0.3 version: 6.0.3 @@ -126,6 +129,9 @@ importers: '@tsconfig/svelte': specifier: ^5.0.4 version: 5.0.4 + '@types/browser-util-inspect': + specifier: ^0.2.4 + version: 0.2.4 '@types/child-process-promise': specifier: ^2.2.6 version: 2.2.6 @@ -1107,6 +1113,9 @@ packages: '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + '@types/browser-util-inspect@0.2.4': + resolution: {integrity: sha512-m4JohXh9JmxXT3dn4TAnAVquJqSiQMPahKNTfO++ZpZ75AB3aMOmDOCDnFvAPvkG13TzStqJDQRgao4CQvIIZw==} + '@types/child-process-promise@2.2.6': resolution: {integrity: sha512-g0pOHijr6Trug43D2bV0PLSIsSHa/xHEES2HeX5BAlduq1vW0nZcq27Zeud5lgmNB+kPYYVqiMap32EHGTco/w==} @@ -1379,6 +1388,9 @@ packages: browser-resolve@2.0.0: resolution: {integrity: sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==} + browser-util-inspect@0.2.0: + resolution: {integrity: sha512-R7WvAj0p9FtwS2Jbtc1HUd1+YZdeb5EEqjBSbbOK3owJtW1viWyJDeTPy43QZ7bZ8POtb1yMv++h844486jMsQ==} + browserify-aes@1.2.0: resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} @@ -3819,6 +3831,8 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 22.10.1 + '@types/browser-util-inspect@0.2.4': {} + '@types/child-process-promise@2.2.6': dependencies: '@types/node': 22.10.1 @@ -4124,6 +4138,8 @@ snapshots: dependencies: resolve: 1.22.8 + browser-util-inspect@0.2.0: {} + browserify-aes@1.2.0: dependencies: buffer-xor: 1.0.3 diff --git a/spa/src/api-client.ts b/spa/src/api-client.ts index 7d2651b..32e9b12 100644 --- a/spa/src/api-client.ts +++ b/spa/src/api-client.ts @@ -53,7 +53,7 @@ export class ApiClient { fileLastCreatedOrModified: new Date(file.fileLastCreatedOrModified), recordCreatedAt: new Date(file.recordCreatedAt), referencedFilePassphrases: file.referencedFilePassphrases - ? decodeCbor(await decryptInlineData(file.referencedFilePassphrases, passphrase)) as Record + ? decodeCbor(await decryptInlineData(file.referencedFilePassphrases, passphrase)) as Record : undefined }; } else { diff --git a/spa/src/markdown-renderer.ts b/spa/src/markdown-renderer.ts index 48f1a86..2cd6639 100644 --- a/spa/src/markdown-renderer.ts +++ b/spa/src/markdown-renderer.ts @@ -52,7 +52,7 @@ export function makeUrl( if (passphrase) { if (!referencedFilePassphrases || !((filePath + '.md') in referencedFilePassphrases)) { console.log('unresolved link:', filePath); - return filePath; + return `#/${route}/${handle}/${getPublicFileRkey({ path: filePath, vaultName: currentVault })}`; } // TODO filePath or realPath here? is this folder handling correct? diff --git a/spa/src/routes.ts b/spa/src/routes.ts index b753059..ce124d0 100644 --- a/spa/src/routes.ts +++ b/spa/src/routes.ts @@ -4,7 +4,7 @@ import NotFound from './routes/NotFound.svelte'; export default { '/': Home, - '/private-page/:handle/:rkey/:passphrase': Page, + '/private-page/:handle/:rkey/:passphrase?': Page, '/page/:handle/:rkey': Page, // The catch-all route must always be last '*': NotFound diff --git a/spa/src/routes/Page.svelte b/spa/src/routes/Page.svelte index d7d0a43..4328f26 100644 --- a/spa/src/routes/Page.svelte +++ b/spa/src/routes/Page.svelte @@ -2,13 +2,24 @@ import { ApiClient } from "../api-client"; import path from 'path-browserify'; import { makeUrl, markdownRender } from "../markdown-renderer"; + import { push, pop, replace } from 'svelte-spa-router' + import { onMount } from "svelte"; + import inspect from 'browser-util-inspect'; export let params: { handle: string; rkey: string; passphrase?: string }; - const handle = params.handle; - const rkey = params.rkey; - const passphrase = params.passphrase; - const route = location.hash.startsWith('#/page/') ? 'page' : 'private-page'; + let handle: string; + let rkey: string; + let passphrase: string | undefined; + let route: string; + + // svelte-spa-router does not re-mount the component when parameters change, so we need to use reactivity + $: { + handle = params.handle; + rkey = params.rkey; + passphrase = params.passphrase; + route = location.hash.startsWith('#/page/') ? 'page' : 'private-page'; + } let htmlElement: HTMLElement; let title: string | undefined; @@ -16,10 +27,27 @@ let textContent: string | undefined; let dom: DocumentFragment; - const filePromise = ApiClient.create(handle) - .then(client => client.getAndDecryptFile(rkey, passphrase)) - .then(afile => file = afile) - .catch(err => console.error(err)); + let filePromise: ReturnType; + + $: { + filePromise = ApiClient.create(params.handle) + .then(client => client.getAndDecryptFile(params.rkey, params.passphrase)) + .then(result => file = result) + + filePromise + .catch(err => console.error(err)); + } + + // add this as an onclick to anchor events to navigate to the page via the router instead + function navigate(event: MouseEvent) { + if (event.button !== 0) return; + + const href = (event.currentTarget as HTMLAnchorElement).href; + if (href.includes('#/')) { + event.preventDefault(); + push(href.slice(href.indexOf('#/'))); + } + } $: { textContent = typeof file?.contents === 'string' @@ -49,8 +77,8 @@ console.log(anchor, href, realPath); - // TODO passphrase support here anchor.setAttribute('href', realPath); + (anchor as HTMLAnchorElement).addEventListener('click', navigate); } } } @@ -62,9 +90,15 @@ href => makeUrl(file!.filePath, file!.vaultName, route, handle, decodeURI(href), passphrase, file!.referencedFilePassphrases) ); + for (const anchor of rendered.element.querySelectorAll('a')) { + if (anchor.href.includes('#')) { + anchor.addEventListener('click', navigate); + } + } + if (rendered.frontmatterYaml) { const pre = document.createElement('pre'); - pre.style.overflowX = 'scroll'; + pre.style.overflowX = 'auto'; pre.append(rendered.frontmatterYaml); htmlElement.append(pre); } @@ -73,34 +107,28 @@ } -
- {#await filePromise} -

Loading file from repo @{handle}

- {:then file} - {#if 'html' in file && file.html} -
-

{path.basename(file.filePath, '.md')}

+{#await filePromise} +

Loading file from repo @{handle}

+{:then file} + {#if 'html' in file && file.html} +
+

{path.basename(file.filePath, '.md')}

+
+
+ {:else if textContent} +
+

{path.basename(file.filePath, '.md')}

+
- {:else if textContent} -
-

{path.basename(file.filePath, '.md')}

-
-
-
-
- {/if} - {:catch err} -

File load error:

-
{err}
- {/await} -
+
+ {/if} +{:catch err} +

File load error:

+
{inspect(err)}
+{/await}