diff --git a/.gitignore b/.gitignore
index 6635cf5..b8bf853 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,8 @@ node_modules
/build
/.svelte-kit
/package
+package-lock.json
+pnpm-lock.yaml
.env
.env.*
!.env.example
diff --git a/package.json b/package.json
index 0636ce9..99ae03a 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,8 @@
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "^8.56.7",
+ "@types/markdown-it": "^14.1.1",
+ "@types/sanitize-html": "^2.11.0",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
@@ -33,8 +35,11 @@
},
"type": "module",
"dependencies": {
+ "@noble/hashes": "^1.4.0",
"@nostr-dev-kit/ndk": "^2.8.2",
"@nostr-dev-kit/ndk-svelte": "^2.2.15",
- "@nostr-dev-kit/ndk-svelte-components": "^2.2.16"
+ "@nostr-dev-kit/ndk-svelte-components": "^2.2.16",
+ "markdown-it": "^14.1.0",
+ "sanitize-html": "^2.13.0"
}
}
diff --git a/src/app.html b/src/app.html
index 77a5ff5..703e4fc 100644
--- a/src/app.html
+++ b/src/app.html
@@ -1,12 +1,18 @@
-
-
-
-
-
- %sveltekit.head%
-
-
+
+
+
+
+
+
+ %sveltekit.head%
+
+
+
+
+
%sveltekit.body%
-
-
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/ndk-kinds/index.ts b/src/lib/ndk-kinds/index.ts
new file mode 100644
index 0000000..7114375
--- /dev/null
+++ b/src/lib/ndk-kinds/index.ts
@@ -0,0 +1,15 @@
+export enum NDKKind {
+ Highlight = 9802,
+ UserList = 30000,
+ GenericList = 30001,
+ HighlightList = 39802,
+ RelayList = 30022,
+ LongForm = 30023,
+}
+
+export const NDKListKinds = [
+ NDKKind.UserList,
+ NDKKind.GenericList,
+ NDKKind.RelayList,
+ NDKKind.HighlightList,
+];
\ No newline at end of file
diff --git a/src/lib/ndk-kinds/lists/index.ts b/src/lib/ndk-kinds/lists/index.ts
new file mode 100644
index 0000000..4268501
--- /dev/null
+++ b/src/lib/ndk-kinds/lists/index.ts
@@ -0,0 +1,184 @@
+import NDK, { NDKEvent, NDKUser, type NDKTag, type NostrEvent, NDKRelay, type NDKSigner, type NDKFilter, mergeFilters } from '@nostr-dev-kit/ndk';
+import { NDKKind } from '../index';
+import { filterForId } from '$lib/utils/index';
+
+/**
+ * Represents any NIP-33 list kind.
+ */
+class NDKList extends NDKEvent {
+ public _encryptedTags: NDKTag[] | undefined;
+
+ /**
+ * Stores the number of bytes the content was before decryption
+ * to expire the cache when the content changes.
+ */
+ private encryptedTagsLength: number | undefined;
+
+ constructor(ndk?: NDK, rawEvent?: NostrEvent) {
+ super(ndk, rawEvent);
+ if (!this.kind) this.kind = NDKKind.GenericList;
+ }
+
+ static from(ndkEvent: NDKEvent): NDKList {
+ return new NDKList(ndkEvent.ndk, ndkEvent.rawEvent());
+ }
+
+ get name(): string | undefined {
+ return this.tagValue('name') ?? this.tagValue('d');
+ }
+
+ set name(name: string | undefined) {
+ this.removeTag('name');
+
+ if (name) {
+ this.tags.push(['name', name]);
+ } else {
+ throw new Error('Name cannot be empty');
+ }
+ }
+
+ get description(): string | undefined {
+ return this.tagValue('description');
+ }
+
+ set description(name: string | undefined) {
+ if (name) {
+ this.tags.push(['description', name]);
+ } else {
+ this.removeTag('description');
+ }
+ }
+
+ /**
+ * Returns the decrypted content of the list.
+ */
+ async encryptedTags(useCache = true): Promise {
+ if (this._encryptedTags && this.encryptedTagsLength === this.content.length && useCache) return this._encryptedTags;
+
+ if (!this.ndk) throw new Error('NDK instance not set');
+ if (!this.ndk.signer) throw new Error('NDK signer not set');
+
+ const user = await this.ndk.signer.user();
+
+ try {
+ if (this.content.length > 0) {
+ try {
+ console.log(`decrypting ${this.content}`);
+ const decryptedContent = await this.ndk.signer.decrypt(user, this.content);
+ const a = JSON.parse(decryptedContent);
+ if (a && a[0]) {
+ this.encryptedTagsLength = this.content.length;
+ return this._encryptedTags = a;
+ }
+ this.encryptedTagsLength = this.content.length;
+ return this._encryptedTags = [];
+ } catch (e) {
+ console.log(`error decrypting ${this.content}`);
+ }
+ }
+ } catch (e) {
+ // console.trace(e);
+ // throw e;
+ }
+
+ return [];
+ }
+
+ public validateTag(tagValue: string): boolean | string {
+ return true;
+ }
+
+ get items(): NDKTag[] {
+ return this.tags.filter((t) => {
+ return !['d', 'name', 'description'].includes(t[0]);
+ });
+ }
+
+ async fetchItems(): Promise> {
+ let filters: NDKFilter[] = []
+
+ if (!this.ndk) throw new Error('NDK instance not set');
+
+ filters = this.items.map(i => filterForId(i[1]));
+
+ console.log(`filters: ${JSON.stringify(filters)}`);
+ console.log(mergeFilters(filters));
+
+ return this.ndk.fetchEvents(
+ mergeFilters(filters)
+ );
+ }
+
+ async addItem(relay: NDKRelay, mark?: string, encrypted?: boolean): Promise;
+ async addItem(event: NDKEvent, mark?: string, encrypted?: boolean): Promise;
+ async addItem(user: NDKUser, mark?: string, encrypted?: boolean): Promise;
+ async addItem(tag: NDKTag, mark?: string, encrypted?: boolean): Promise;
+ async addItem(obj: NDKUser | NDKEvent | NDKRelay | NDKTag, mark: string | undefined = undefined, encrypted: boolean = false): Promise {
+ if (!this.ndk) throw new Error('NDK instance not set');
+ if (!this.ndk.signer) throw new Error('NDK signer not set');
+
+ let tag;
+
+ if (obj instanceof NDKEvent) {
+ tag = obj.tagReference();
+ } else if (obj instanceof NDKUser) {
+ tag = obj.tagReference();
+ } else if (obj instanceof NDKRelay) {
+ tag = ['r', (obj as NDKRelay).url];
+ } else if (Array.isArray(obj)) {
+ // NDKTag
+ tag = obj;
+ } else {
+ throw new Error('Invalid object type');
+ }
+
+ if (mark) tag.push(mark);
+
+ if (encrypted) {
+ const user = await this.ndk.signer.user();
+ const currentList = await this.encryptedTags();
+
+ console.log(`current list: ${JSON.stringify(currentList)}`)
+
+ currentList.push(tag);
+ this._encryptedTags = currentList;
+ this.encryptedTagsLength = this.content.length;
+ this.content = JSON.stringify(currentList);
+ await this.encrypt(user);
+ } else {
+ this.tags.push(tag);
+ }
+
+ this.created_at = Math.floor(Date.now() / 1000);
+ }
+
+ /**
+ * Removes an item from the list.
+ *
+ * @param index The index of the item to remove.
+ * @param encrypted Whether to remove from the encrypted list or not.
+ */
+ async removeItem(index: number, encrypted: boolean): Promise {
+ if (!this.ndk) throw new Error('NDK instance not set');
+ if (!this.ndk.signer) throw new Error('NDK signer not set');
+
+ if (encrypted) {
+ const user = await this.ndk.signer.user();
+ const currentList = await this.encryptedTags();
+
+ currentList.splice(index, 1);
+ this._encryptedTags = currentList;
+ this.encryptedTagsLength = this.content.length;
+ this.content = JSON.stringify(currentList);
+ await this.encrypt(user);
+ } else {
+ this.tags.splice(index, 1);
+ }
+
+ this.created_at = Math.floor(Date.now() / 1000);
+
+ return this;
+ }
+}
+
+export default NDKList;
\ No newline at end of file
diff --git a/src/lib/ndk-kinds/lists/relay-list.ts b/src/lib/ndk-kinds/lists/relay-list.ts
new file mode 100644
index 0000000..1ffb00a
--- /dev/null
+++ b/src/lib/ndk-kinds/lists/relay-list.ts
@@ -0,0 +1,31 @@
+import type { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk';
+import NDKList from './index.js';
+import type NDK from '@nostr-dev-kit/ndk';
+
+class NDKRelayList extends NDKList {
+ constructor(ndk?: NDK, rawEvent?: NostrEvent) {
+ super(ndk, rawEvent);
+ }
+
+ static from(ndkEvent: NDKEvent) {
+ return new NDKRelayList(ndkEvent.ndk, ndkEvent.rawEvent());
+ }
+
+ public validateTag(tagValue: string): boolean | string {
+ if (tagValue.startsWith('wss://')) return true;
+ if (tagValue.startsWith('nrelay')) return true;
+
+ return 'Invalid relay URL';
+ }
+
+ /**
+ * Get the list of relays
+ */
+ get relays(): string[] {
+ console.log(this.getMatchingTags('r'));
+
+ return this.getMatchingTags('r').map((tag) => tag[1]);
+ }
+}
+
+export default NDKRelayList;
\ No newline at end of file
diff --git a/src/lib/signers/ephemeral.ts b/src/lib/signers/ephemeral.ts
new file mode 100644
index 0000000..c893781
--- /dev/null
+++ b/src/lib/signers/ephemeral.ts
@@ -0,0 +1,142 @@
+import NDK, { NDKEvent, NDKPrivateKeySigner, type NDKFilter, type NDKSigner, type NDKUserProfile } from "@nostr-dev-kit/ndk";
+import type { NDKTag, NostrEvent} from "@nostr-dev-kit/ndk";
+import { sha256 } from "@noble/hashes/sha256";
+import { bytesToHex } from "@noble/hashes/utils";
+
+const CURRENT_PRIVATE_NOTE_VERSION = '2';
+
+interface IFindEphemeralSignerLookups {
+ name?: string,
+ associatedEventNip19?: string;
+}
+
+/**
+ * Find a named ephemeral signer from a self-DM.
+ */
+export async function findEphemeralSigner(
+ ndk: NDK,
+ mainSigner: NDKSigner,
+ opts: IFindEphemeralSignerLookups
+): Promise {
+ const mainUser = await mainSigner.user();
+ const filter: NDKFilter = { kinds: [2600 as number]};
+
+ if (opts.name) {
+ const hashedName = await getHashedKeyName(opts.name);
+ filter['#e'] = [hashedName];
+ } else if (opts.associatedEventNip19) {
+ const hashedEventReference = await getHashedKeyName(opts.associatedEventNip19);
+ filter['#e'] = [hashedEventReference];
+ }
+
+ const event = await ndk.fetchEvent(filter);
+
+ if (event) {
+ const decryptEventFunction = async (event: NDKEvent) => {
+ await event.decrypt(await mainSigner.user());
+ const content = JSON.parse(event.content);
+ return new NDKPrivateKeySigner(content.key);
+ };
+
+ const promise = new Promise((resolve, reject) => {
+ let decryptionAttempts = 0;
+ try {
+ decryptionAttempts++;
+ resolve(decryptEventFunction(event));
+ } catch(e) {
+ if (decryptionAttempts> 5) {
+ console.error(`Failed to decrypt ephemeral signer event after ${decryptionAttempts} attempts`);
+ reject(e);
+ return;
+ }
+ setTimeout(() => { decryptEventFunction(event);}, 1000 * Math.random());
+ }
+ });
+ return promise;
+ }
+}
+
+type EphemeralKeyEventContent = {
+ key: string;
+ event?: string;
+ version: string;
+ metadata?: object;
+}
+
+interface ISaveOpts {
+ associatedEvent?: NDKEvent;
+ name?: string;
+ metadata?: object;
+ keyProfile?: NDKUserProfile;
+ mainSigner?: NDKSigner;
+}
+
+function generateContent(targetSigner: NDKPrivateKeySigner, opts: ISaveOpts = {}) {
+ const content: EphemeralKeyEventContent = {
+ key: targetSigner.privateKey!,
+ version: CURRENT_PRIVATE_NOTE_VERSION,
+ ...opts.metadata,
+ };
+ if (opts.associatedEvent) content.event = opts.associatedEvent.encode();
+
+ return JSON.stringify(content);
+}
+
+async function getHashedKeyName(name:string) {
+ let eventHash = sha256(name);
+ return bytesToHex(eventHash);
+}
+
+async function generateTags(mainSigner:NDKSigner, opts: ISaveOpts = {}) {
+ const mainUser = await mainSigner.user();
+ const tags = [
+ ['p', mainUser.pubkey],
+ ['client', 'cofabricate'],
+ ]
+
+ if (opts.associatedEvent) {
+ const hashedEventReference = await getHashedKeyName(opts.associatedEvent.encode());
+ tags.push(['e', hashedEventReference]);
+ }
+
+ if (opts.name) {
+ const hashedName = await getHashedKeyName(opts.name);
+ tags.push(['e', hashedName]);
+ }
+
+ return tags;
+}
+
+export async function saveEphemeralSigner(ndk: NDK, targetSigner: NDKPrivateKeySigner, opts: ISaveOpts = {}) {
+ const mainSigner = opts.mainSigner || ndk.signer;
+
+ if (!mainSigner) throw new Error('No main signer provided');
+
+ const mainUser = await mainSigner.user();
+ const event = new NDKEvent(ndk, {
+ kind: 2600,
+ content: generateContent(targetSigner, opts),
+ tags: await generateTags(mainSigner, opts),
+ } as NostrEvent);
+
+ event.pubkey = mainUser.pubkey;
+ await event.encrypt(mainUser, mainSigner);
+ await event.publish();
+
+ if (opts.keyProfile) {
+ const user = await targetSigner.user();
+ const event = new NDKEvent(ndk, {
+ kind: 0,
+ content: JSON.stringify(opts.keyProfile),
+ tags: [] as NDKTag[],
+ } as NostrEvent)
+ event.pubkey = user.pubkey;
+ await event.sign(targetSigner);
+ await event.publish();
+ }
+}
+
+export function generateEphemeralSigner(): NDKPrivateKeySigner {
+ const signer = NDKPrivateKeySigner.generate();
+ return signer;
+}
\ No newline at end of file
diff --git a/src/lib/store.ts b/src/lib/store.ts
new file mode 100644
index 0000000..1800c2e
--- /dev/null
+++ b/src/lib/store.ts
@@ -0,0 +1,24 @@
+import { writable } from "svelte/store";
+import { NDKUser } from "@nostr-dev-kit/ndk";
+
+export const currentUser = writable(null);
+export const currentUserFollowPubkeys = writable(undefined);
+export const backgroundBanner = writable(null);
+
+export type ScopeSelection = {
+ label: string;
+ id: string;
+ pubkeys: string[] | undefined;
+};
+export const currentScope = writable({
+ label: 'global',
+ id: 'global',
+ pubkeys: undefined,
+});
+
+let zapEvent: any;
+
+export const zap = writable(zapEvent);
+
+export const pageTitle = writable(null);
+export const pageSubtitle = writable(null);
\ No newline at end of file
diff --git a/src/lib/stores/nostr.ts b/src/lib/stores/nostr.ts
new file mode 100644
index 0000000..d08ac1d
--- /dev/null
+++ b/src/lib/stores/nostr.ts
@@ -0,0 +1,35 @@
+import { writable } from "svelte/store";
+import NDK from "@nostr-dev-kit/ndk";
+import NDKSvelte from "@nostr-dev-kit/ndk-svelte";
+import { RelayList } from "@nostr-dev-kit/ndk-svelte-components";
+
+let relays;
+
+try {
+ relays = localStorage.getItem('relays');
+} catch (e) {}
+
+let relayList: string[] = [];
+
+if (relays) {
+ relayList = JSON.parse(relays);
+}
+
+export const defaultRelays = [
+ 'wss://purplepag.es',
+ 'wss://relay.damus.io'
+]
+
+if (!relayList || !Array.isArray(relayList) || relayList.length === 0) {
+ relayList = defaultRelays;
+}
+
+const _ndk: NDKSvelte = new NDKSvelte({
+ devWriteRelayUrls: ['wss://relay.strfront.com'],
+ explicitRelayUrls: relayList,
+ enableOutboxModel: true,
+}) as NDKSvelte;
+
+const ndk = writable(_ndk);
+
+export default ndk;
\ No newline at end of file
diff --git a/src/lib/stores/signer.ts b/src/lib/stores/signer.ts
new file mode 100644
index 0000000..9f39b49
--- /dev/null
+++ b/src/lib/stores/signer.ts
@@ -0,0 +1,82 @@
+import { findEphemeralSigner } from "$lib/signers/ephemeral";
+import { NDKPrivateKeySigner, type NDKSigner, type NDKUser } from "@nostr-dev-kit/ndk";
+import { writable, get as getStore, derived } from "svelte/store";
+import ndkStore from "./nostr";
+import { currentUser as currentUserStore } from "../store";
+import type NDKList from "$lib/ndk-kinds/lists";
+
+export type SignerStoreItem = {
+ signer: NDKPrivateKeySigner;
+ user: NDKUser;
+ saved: boolean;
+ name?: string;
+ id: string;
+};
+
+type SignerItems = Map;
+
+export const signers = writable(new Map());
+export const npubSigners = derived(signers, ($signers) => {
+ const npubs = new Map();
+
+ for (const entry of $signers) {
+ const { user, signer } = entry[1];
+
+ npubs.set(user.npub, signer);
+ }
+
+ return npubs;
+});
+
+async function getDelegatedSignerName(list: NDKList) {
+ let name = '';
+ const currentUser: NDKUser = getStore(currentUserStore);
+
+ if (!currentUser?.profile) {
+ currentUser.ndk = getStore(ndkStore);
+ await currentUser?.fetchProfile();
+ }
+
+ if (currentUser?.profile?.name) {
+ name = currentUser.profile.displayName + `'s `;
+ }
+
+ return name + list.name;
+}
+
+export async function getSigner(list: NDKList): Promise {
+ const store = getStore(signers);
+ const id = list.encode();
+ let item = store.get(id);
+
+ if (item) return item;
+
+ const ndk = getStore(ndkStore);
+ let signer = await findEphemeralSigner(ndk, ndk.signer!, {
+ associatedEventNip19: list.encode(),
+ });
+
+ if (signer) {
+ console.log(`found a signer for list ${list.name}`);
+ item = {
+ signer: signer!,
+ user: await signer.user(),
+ saved: true,
+ id,
+ };
+ } else {
+ signer = NDKPrivateKeySigner.generate();
+ item = {
+ signer,
+ user: await signer.user(),
+ saved: false,
+ name: await getDelegatedSignerName(list),
+ id,
+ };
+ }
+ item.user.ndk = ndk;
+
+ store.set(id, item);
+
+ return item;
+}
\ No newline at end of file
diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts
new file mode 100644
index 0000000..2b6c075
--- /dev/null
+++ b/src/lib/utils/index.ts
@@ -0,0 +1,190 @@
+import sanitizeHtml from 'sanitize-html';
+import MarkdownIt from 'markdown-it';
+import { nip19 } from 'nostr-tools';
+import { filterFromId, type NDKFilter, type NDKTag } from '@nostr-dev-kit/ndk';
+
+export function tagToNip19(tag: NDKTag): string | undefined {
+ switch (tag[0]) {
+ case 'a':
+ const [ kind, pubkey, identifier ] = tag[1].split(':');
+ return nip19.naddrEncode({
+ kind: parseInt(kind),
+ pubkey,
+ identifier
+ })
+ case 'e':
+ return nip19.noteEncode(tag[1]);
+ case 'r':
+ return tag[1];
+ }
+}
+
+export function nicelyFormattedMilliSatNumber(amount: number) {
+ return nicelyFormattedSatNumber(
+ Math.floor(amount / 1000)
+ );
+}
+
+export function nicelyFormattedSatNumber(amount: number) {
+ // if the number is less than 1000, just return it
+ if (amount < 1000) return amount;
+
+ // if the number is less than 1 million, return it with a k, if the comma is not needed remove it
+ if (amount < 1000000) return `${(amount / 1000).toFixed(0)}k`;
+
+ // if the number is less than 1 billion, return it with an m
+ if (amount < 1000000000) return `${(amount / 1000000).toFixed(1)}m`;
+
+ return `${(amount / 100_000_000).toFixed(2)} btc`;
+}
+
+export function filterForId(id: string): NDKFilter {
+ if (!!id.match(/:/)) {
+ const [kind, pubkey, identifier] = id.split(':');
+ return { kinds: [parseInt(kind)], authors: [pubkey], '#d': [identifier] };
+ } else {
+ return { ids: [id] };
+ }
+}
+
+export function filterFromNaddr(naddr: string): NDKFilter {
+ return filterFromId(naddr);
+}
+
+export function idFromNaddr(naddr: string) {
+ const ndecode = nip19.decode(naddr).data as any;
+ return `${ndecode.kind}:${ndecode.pubkey}:${ndecode.identifier}`;
+}
+
+export function naddrFromTagValue(value: string) {
+ const [kind, pubkey, identifier] = value.split(':');
+
+ return nip19.naddrEncode({
+ kind: parseInt(kind),
+ pubkey,
+ identifier
+ });
+}
+
+export function prettifyContent(content: string) {
+ const bitcoinImage =
+ '
';
+
+ content = content.replace(/#bitcoin/i, `#bitcoin${bitcoinImage}`);
+
+ const md = new MarkdownIt({
+ html: true,
+ linkify: true,
+ typographer: true,
+ autolink: true,
+ image: true,
+ });
+ md.linkify?.set();
+ content = md.render(content);
+
+ return sanitizeHtml(content);
+}
+
+function flattenText(node: string) {
+ const texts = [];
+ const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
+
+ while (walker.nextNode()) {
+ texts.push(walker.currentNode);
+ }
+
+ return texts;
+}
+
+export function highlightText(targetText: string, highlightId: string) {
+ const regex = new RegExp(escapeRegExp(targetText), 'g');
+ const textNodes = flattenText(document.body);
+ const marks: HTMLElement[] = [];
+
+ textNodes.forEach((textNode) => {
+ let match;
+ let lastIndex = 0;
+
+ while ((match = regex.exec(textNode.data)) !== null) {
+ const mark = document.createElement('mark');
+ mark.setAttribute('data-highlight-id', highlightId);
+ const range = document.createRange();
+ const startOffset = match.index;
+ const endOffset = startOffset + targetText.length;
+
+ if (lastIndex < startOffset) {
+ const precedingTextNode = document.createTextNode(textNode.data.slice(lastIndex, startOffset));
+ textNode.parentNode.insertBefore(precedingTextNode, textNode);
+ }
+
+ range.setStart(textNode, startOffset);
+ range.setEnd(textNode, endOffset);
+ range.surroundContents(mark);
+ marks.push(mark);
+
+ lastIndex = endOffset;
+ }
+
+ if (lastIndex < textNode.length) {
+ const remainingTextNode = document.createTextNode(textNode.data.slice(lastIndex));
+ textNode.parentNode.insertBefore(remainingTextNode, textNode);
+ }
+
+ textNode.remove();
+ });
+
+ return marks;
+
+ function escapeRegExp(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
+ }
+}
+
+// Usage example: highlightText("your target text here");
+
+// export function modifyDocument(content: string) {
+// const regex = new RegExp(content, 'gi');
+
+// // get all the text nodes in the current page
+// const textNodes = document.evaluate("//text()", document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
+
+// // loop through all the text nodes and replace the search string with the marked up version
+// for (let i = 0; i < textNodes.snapshotLength; i++) {
+// const node = textNodes.snapshotItem(i) as Text;
+// const parent = node.parentNode;
+
+// // check if the text node contains the search string
+// if (!node || !node.textContent || !node.textContent.match(regex)) {
+// continue;
+// }
+
+// const fragment = document.createDocumentFragment();
+// let match;
+
+// if (!node) continue;
+
+// // loop through all the matches in the text node
+// while ((match = regex.exec(node.textContent)) !== null) {
+// console.log({match, node});
+
+// const before = node.splitText(match.index);
+// const after = node.splitText(match[0].length);
+
+// console.log({before, after});
+
+// const mark = document.createElement("mark");
+
+// // append the matched text to the mark element
+// mark.appendChild(document.createTextNode(match[0]));
+
+// // append the mark element to the document fragment
+// fragment.appendChild(before);
+// fragment.appendChild(mark);
+
+// node = after;
+// }
+
+// // replace the original text node with the marked up version
+// parent.replaceChild(fragment, node);
+// }
+// }
\ No newline at end of file
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
new file mode 100644
index 0000000..9fbb9ca
--- /dev/null
+++ b/src/routes/+layout.svelte
@@ -0,0 +1,11 @@
+
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 16f03a0..2b6124f 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1 +1,5 @@
-Welcome to CoFabricate
\ No newline at end of file
+
+ Decentralized Manufacturing
+
+
+Home
\ No newline at end of file
diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte
new file mode 100644
index 0000000..cdb72a8
--- /dev/null
+++ b/src/routes/about/+page.svelte
@@ -0,0 +1,5 @@
+
+ About
+
+
+About page
\ No newline at end of file
diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte
new file mode 100644
index 0000000..fc2f17d
--- /dev/null
+++ b/src/routes/login/+page.svelte
@@ -0,0 +1,5 @@
+
+ Log In
+
+
+Log In
n
\ No newline at end of file