diff --git a/main.go b/main.go
index a9ac728..d25f72f 100644
--- a/main.go
+++ b/main.go
@@ -23,9 +23,9 @@ import (
"text/template"
)
-const VERSION = "v1"
+const VERSION = "v2"
const CONTEXT_USER = 1
-const SYNC_PAGINATION = 100
+const SYNC_PAGINATION = 200
var (
FlagGenerate bool
@@ -132,6 +132,7 @@ func main() { // {{{
http.HandleFunc("/notes2", pageNotes2)
http.HandleFunc("/login", pageLogin)
http.HandleFunc("/sync", pageSync)
+ http.HandleFunc("/offline", pageOffline)
http.HandleFunc("/user/authenticate", AuthManager.AuthenticationHandler)
@@ -226,6 +227,15 @@ func pageServiceWorker(w http.ResponseWriter, r *http.Request) { // {{{
return
}
} // }}}
+func pageOffline(w http.ResponseWriter, r *http.Request) { // {{{
+ page := NewPage("offline")
+
+ err := Webengine.Render(page, w, r)
+ if err != nil {
+ w.Write([]byte(err.Error()))
+ return
+ }
+} // }}}
func pageLogin(w http.ResponseWriter, r *http.Request) { // {{{
page := NewPage("login")
@@ -269,9 +279,11 @@ func actionSyncFromServer(w http.ResponseWriter, r *http.Request) { // {{{
return
}
+ /*
Log.Debug("/sync/from_server", "num_nodes", len(nodes), "maxSeq", maxSeq)
foo, _ := json.Marshal(nodes)
os.WriteFile(fmt.Sprintf("/tmp/nodes-%d.json", offset), foo, 0644)
+ */
j, _ := json.Marshal(struct {
OK bool
@@ -288,7 +300,6 @@ func actionSyncFromServerCount(w http.ResponseWriter, r *http.Request) { // {{{
user := getUser(r)
changedFrom, _ := strconv.Atoi(r.PathValue("sequence"))
- Log.Debug("FOO", "UUID", user.ClientUUID, "changedFrom", changedFrom)
count, err := NodesCount(user.UserID, uint64(changedFrom), user.ClientUUID)
if err != nil {
Log.Error("/sync/from_server/count", "error", err)
@@ -334,9 +345,14 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
return
}
- db.Exec(`CALL add_nodes($1, $2, $3::jsonb)`, user.UserID, user.ClientUUID, request.NodeData)
+ _, err = db.Exec(`CALL add_nodes($1, $2, $3::jsonb)`, user.UserID, user.ClientUUID, request.NodeData)
+ if err != nil {
+ Log.Error("sync", "error", err)
+ httpError(w, err)
+ return
+ }
- responseData(w, map[string]interface{}{
+ responseData(w, map[string]any{
"OK": true,
})
} // }}}
diff --git a/sql/00006.sql b/sql/00006.sql
index 6b0ea9b..453b260 100644
--- a/sql/00006.sql
+++ b/sql/00006.sql
@@ -1,16 +1 @@
-CREATE TABLE public.node_history (
- id serial4 NOT NULL,
- user_id int4 NOT NULL,
- uuid bpchar(36) NOT NULL,
- parents varchar[] NULL,
- created timestamptz NOT NULL,
- updated timestamptz NOT NULL,
- name varchar(256) NOT NULL,
- "content" text NOT NULL,
- content_encrypted text NOT NULL,
- markdown bool DEFAULT false NOT NULL,
- client bpchar(36) DEFAULT ''::bpchar NOT NULL,
- CONSTRAINT node_history_pk PRIMARY KEY (id),
- CONSTRAINT node_history_user_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE RESTRICT ON UPDATE RESTRICT
-);
-CREATE INDEX node_history_uuid_idx ON public.node USING btree (uuid);
+DROP INDEX public.node_uuid_idx;
diff --git a/sql/00011.sql b/sql/00011.sql
new file mode 100644
index 0000000..5b67839
--- /dev/null
+++ b/sql/00011.sql
@@ -0,0 +1,166 @@
+CREATE OR REPLACE PROCEDURE add_nodes(p_user_id int4, p_client_uuid varchar, p_nodes jsonb)
+LANGUAGE PLPGSQL AS $$
+
+DECLARE
+ node_data jsonb;
+ node_updated timestamptz;
+ db_updated timestamptz;
+ db_uuid bpchar;
+ db_client bpchar;
+ db_client_seq int;
+ node_uuid bpchar;
+ parent_uuid bpchar;
+
+BEGIN
+ RAISE NOTICE '--------------------------';
+ FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes)
+ LOOP
+ node_uuid = (node_data->>'UUID')::bpchar;
+ node_updated = (node_data->>'Updated')::timestamptz;
+
+ IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' THEN
+ parent_uuid = NULL;
+ ELSE
+ parent_uuid = node_data->>'ParentUUID';
+ END IF;
+
+ /* Retrieve the current modified timestamp for this node from the database. */
+ SELECT
+ uuid, updated, client, client_sequence
+ INTO
+ db_uuid, db_updated, db_client, db_client_seq
+ FROM public."node"
+ WHERE
+ user_id = p_user_id AND
+ uuid = node_uuid;
+
+ /* Is the node not in database? It needs to be created. */
+ IF db_uuid IS NULL THEN
+ RAISE NOTICE '01 New node %', node_uuid;
+ INSERT INTO public."node" (
+ user_id, "uuid", parent_uuid, created, updated,
+ "name", "content", markdown, "content_encrypted",
+ client, client_sequence
+ )
+ VALUES(
+ p_user_id,
+ node_uuid,
+ parent_uuid,
+ (node_data->>'Created')::timestamptz,
+ (node_data->>'Updated')::timestamptz,
+ (node_data->>'Name')::varchar,
+ (node_data->>'Content')::text,
+ (node_data->>'Markdown')::bool,
+ '', /* content_encrypted */
+ p_client_uuid,
+ (node_data->>'ClientSequence')::int
+ );
+ CONTINUE;
+ END IF;
+
+
+ /* The client could send a specific node again if it didn't receive the OK from this procedure before. */
+ IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN
+ RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq;
+ CONTINUE;
+ END IF;
+
+ /* Determine if the incoming node data is to go into history or replace the current node. */
+ IF db_updated > node_updated THEN
+ RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid;
+ /* Incoming node is going straight to history since it is older than the current node. */
+ INSERT INTO node_history(
+ user_id, "uuid", parents, created, updated,
+ "name", "content", markdown, "content_encrypted",
+ client, client_sequence
+ )
+ VALUES(
+ p_user_id,
+ node_uuid,
+ (jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors",
+ (node_data->>'Created')::timestamptz,
+ (node_data->>'Updated')::timestamptz,
+ (node_data->>'Name')::varchar,
+ (node_data->>'Content')::text,
+ (node_data->>'Markdown')::bool,
+ '', /* content_encrypted */
+ p_client_uuid,
+ (node_data->>'ClientSequence')::int
+ )
+ ON CONFLICT (client, client_sequence)
+ DO NOTHING;
+ ELSE
+ RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence');
+ /* Incoming node is newer and will replace the current node.
+ *
+ * The current node is copied to the node_history table and then modified in place
+ * with the incoming data. */
+ INSERT INTO node_history(
+ user_id, "uuid", parents,
+ created, updated, "name", "content", markdown, "content_encrypted",
+ client, client_sequence
+ )
+ SELECT
+ user_id,
+ "uuid",
+ (
+ WITH RECURSIVE nodes AS (
+ SELECT
+ uuid,
+ COALESCE(parent_uuid, '') AS parent_uuid,
+ name,
+ 0 AS depth
+ FROM node
+ WHERE
+ uuid = node_uuid
+
+ UNION
+
+ SELECT
+ n.uuid,
+ COALESCE(n.parent_uuid, '') AS parent_uuid,
+ n.name,
+ nr.depth+1 AS depth
+ FROM node n
+ INNER JOIN nodes nr ON n.uuid = nr.parent_uuid
+ )
+ SELECT ARRAY (
+ SELECT name
+ FROM nodes
+ ORDER BY depth DESC
+ OFFSET 1 /* discard itself */
+ )
+ ),
+ created,
+ updated,
+ name,
+ content,
+ markdown,
+ content_encrypted,
+ client,
+ client_sequence
+ FROM public."node"
+ WHERE
+ user_id = p_user_id AND
+ uuid = node_uuid
+ ON CONFLICT (client, client_sequence)
+ DO NOTHING;
+
+ /* Current node in database is updated with incoming data. */
+ UPDATE public."node"
+ SET
+ updated = (node_data->>'Updated')::timestamptz,
+ updated_seq = nextval('node_updates'),
+ name = (node_data->>'Name')::varchar,
+ content = (node_data->>'Content')::text,
+ markdown = (node_data->>'Markdown')::bool,
+ client = p_client_uuid,
+ client_sequence = (node_data->>'ClientSequence')::int
+ WHERE
+ user_id = p_user_id AND
+ uuid = node_uuid;
+ END IF;
+
+ END LOOP;
+END
+$$;
diff --git a/sql/00012.sql b/sql/00012.sql
new file mode 100644
index 0000000..e62f011
--- /dev/null
+++ b/sql/00012.sql
@@ -0,0 +1,166 @@
+CREATE OR REPLACE PROCEDURE add_nodes(p_user_id int4, p_client_uuid varchar, p_nodes jsonb)
+LANGUAGE PLPGSQL AS $$
+
+DECLARE
+ node_data jsonb;
+ node_updated timestamptz;
+ db_updated timestamptz;
+ db_uuid bpchar;
+ db_client bpchar;
+ db_client_seq int;
+ node_uuid bpchar;
+ parent_uuid_nullable bpchar;
+
+BEGIN
+ RAISE NOTICE '--------------------------';
+ FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes)
+ LOOP
+ node_uuid = (node_data->>'UUID')::bpchar;
+ node_updated = (node_data->>'Updated')::timestamptz;
+
+ IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' THEN
+ parent_uuid_nullable = NULL;
+ ELSE
+ parent_uuid_nullable = node_data->>'ParentUUID';
+ END IF;
+
+ /* Retrieve the current modified timestamp for this node from the database. */
+ SELECT
+ uuid, updated, client, client_sequence
+ INTO
+ db_uuid, db_updated, db_client, db_client_seq
+ FROM public."node"
+ WHERE
+ user_id = p_user_id AND
+ uuid = node_uuid;
+
+ /* Is the node not in database? It needs to be created. */
+ IF db_uuid IS NULL THEN
+ RAISE NOTICE '01 New node %', node_uuid;
+ INSERT INTO public."node" (
+ user_id, "uuid", parent_uuid, created, updated,
+ "name", "content", markdown, "content_encrypted",
+ client, client_sequence
+ )
+ VALUES(
+ p_user_id,
+ node_uuid,
+ parent_uuid_nullable,
+ (node_data->>'Created')::timestamptz,
+ (node_data->>'Updated')::timestamptz,
+ (node_data->>'Name')::varchar,
+ (node_data->>'Content')::text,
+ (node_data->>'Markdown')::bool,
+ '', /* content_encrypted */
+ p_client_uuid,
+ (node_data->>'ClientSequence')::int
+ );
+ CONTINUE;
+ END IF;
+
+
+ /* The client could send a specific node again if it didn't receive the OK from this procedure before. */
+ IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN
+ RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq;
+ CONTINUE;
+ END IF;
+
+ /* Determine if the incoming node data is to go into history or replace the current node. */
+ IF db_updated > node_updated THEN
+ RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid;
+ /* Incoming node is going straight to history since it is older than the current node. */
+ INSERT INTO node_history(
+ user_id, "uuid", parents, created, updated,
+ "name", "content", markdown, "content_encrypted",
+ client, client_sequence
+ )
+ VALUES(
+ p_user_id,
+ node_uuid,
+ (jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors",
+ (node_data->>'Created')::timestamptz,
+ (node_data->>'Updated')::timestamptz,
+ (node_data->>'Name')::varchar,
+ (node_data->>'Content')::text,
+ (node_data->>'Markdown')::bool,
+ '', /* content_encrypted */
+ p_client_uuid,
+ (node_data->>'ClientSequence')::int
+ )
+ ON CONFLICT (client, client_sequence)
+ DO NOTHING;
+ ELSE
+ RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence');
+ /* Incoming node is newer and will replace the current node.
+ *
+ * The current node is copied to the node_history table and then modified in place
+ * with the incoming data. */
+ INSERT INTO node_history(
+ user_id, "uuid", parents,
+ created, updated, "name", "content", markdown, "content_encrypted",
+ client, client_sequence
+ )
+ SELECT
+ user_id,
+ "uuid",
+ (
+ WITH RECURSIVE nodes AS (
+ SELECT
+ uuid,
+ COALESCE(parent_uuid, '') AS parent_uuid,
+ name,
+ 0 AS depth
+ FROM node
+ WHERE
+ uuid = node_uuid
+
+ UNION
+
+ SELECT
+ n.uuid,
+ COALESCE(n.parent_uuid, '') AS parent_uuid,
+ n.name,
+ nr.depth+1 AS depth
+ FROM node n
+ INNER JOIN nodes nr ON n.uuid = nr.parent_uuid
+ )
+ SELECT ARRAY (
+ SELECT name
+ FROM nodes
+ ORDER BY depth DESC
+ OFFSET 1 /* discard itself */
+ )
+ ),
+ created,
+ updated,
+ name,
+ content,
+ markdown,
+ content_encrypted,
+ client,
+ client_sequence
+ FROM public."node"
+ WHERE
+ user_id = p_user_id AND
+ uuid = node_uuid
+ ON CONFLICT (client, client_sequence)
+ DO NOTHING;
+
+ /* Current node in database is updated with incoming data. */
+ UPDATE public."node"
+ SET
+ updated = (node_data->>'Updated')::timestamptz,
+ updated_seq = nextval('node_updates'),
+ name = (node_data->>'Name')::varchar,
+ content = (node_data->>'Content')::text,
+ markdown = (node_data->>'Markdown')::bool,
+ client = p_client_uuid,
+ client_sequence = (node_data->>'ClientSequence')::int
+ WHERE
+ user_id = p_user_id AND
+ uuid = node_uuid;
+ END IF;
+
+ END LOOP;
+END
+$$;
diff --git a/static/css/login.css b/static/css/login.css
index 88a9140..7e19cb8 100644
--- a/static/css/login.css
+++ b/static/css/login.css
@@ -1,37 +1,44 @@
+@import "theme.css";
+
#app {
- display: grid;
- justify-items: center;
- margin-top: 128px;
+ display: grid;
+ justify-items: center;
+ margin-top: 128px;
}
+
#logo {
- margin-bottom: 48px;
+ margin-bottom: 48px;
}
+
#box {
- display: grid;
- grid-gap: 16px 0;
- justify-items: center;
- width: 300px;
- padding: 48px 0px;
- background-color: #fff;
- box-shadow: 0px 20px 52px -33px rgba(0, 0, 0, 0.75);
- border-left: 8px solid #666;
-}
-#box input {
- padding: 4px 8px;
- font-size: 1em;
- width: calc(100% - 64px);
- border: 1px solid #aaa;
- border-radius: 4px;
-}
-#box button {
- padding: 6px 16px;
- font-size: 1em;
- border-radius: 4px;
- border: none;
- background-color: #fe5f55;
- color: #fff;
-}
-#box #error {
- color: #c33;
- margin-top: 16px;
+ display: grid;
+ grid-gap: 16px 0;
+ justify-items: center;
+ width: 300px;
+ padding: 48px 0px;
+ background-color: #fff;
+ box-shadow: 0px 20px 52px -33px rgba(0,0,0,0.75);
+ border-left: 8px solid var(--color3);
+
+ input {
+ padding: 4px 8px;
+ font-size: 1em;
+ width: calc(100% - 64px);
+ border: 1px solid #aaa;
+ border-radius: 4px;
+ }
+
+ button {
+ padding: 6px 16px;
+ font-size: 1em;
+ border-radius: 4px;
+ border: none;
+ background-color: var(--color1);
+ color: #fff;
+ }
+
+ #error {
+ color: #c33;
+ margin-top: 16px;
+ }
}
diff --git a/static/css/main.css b/static/css/main.css
index 75f1925..a8924d9 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -1,23 +1,29 @@
+@import "theme.css";
+
html {
- box-sizing: border-box;
- background: #efede8;
- font-family: "Liberation Mono", monospace;
- font-size: 14px;
- margin: 0px;
- padding: 0px;
+ box-sizing: border-box;
+ background: var(--color2);
+ font-family: "Liberation Mono", monospace;
+ font-size: 14px;
+ margin: 0px;
+ padding: 0px;
}
+
body {
- margin: 0px;
- padding: 0px;
+ margin: 0px;
+ padding: 0px;
}
+
*,
*:before,
*:after {
- box-sizing: inherit;
+ box-sizing: inherit;
}
+
*:focus {
- outline: none;
+ outline: none;
}
+
[onClick] {
- cursor: pointer;
+ cursor: pointer;
}
diff --git a/static/css/markdown.css b/static/css/markdown.css
new file mode 100644
index 0000000..84eb0b2
--- /dev/null
+++ b/static/css/markdown.css
@@ -0,0 +1,76 @@
+.el-node-markdown {
+ h1 {
+ border-bottom: 1px solid #ccc;
+ margin-top: 32px;
+ margin-bottom: 8px;
+
+ display: inline-block;
+ font-size: 1.25em;
+
+ border-radius: 8px;
+ color: #fff;
+ background-color: var(--color1);
+ padding: 4px 12px;
+
+ &:first-child {
+ margin-top: 32px;
+ }
+ }
+
+ h2 {
+ font-size: 1.25em;
+ margin-top: 32px;
+ margin-bottom: 0px;
+ color: var(--color1);
+ }
+
+ h3:before {
+ font-size: 1.0em;
+ content: "> ";
+ color: var(--color1);
+ }
+
+ p {
+ line-height: 150%;
+ }
+
+ img {
+ max-width: var(--thumbnail-width);
+ max-height: var(--thumbnail-height);
+ }
+
+ table {
+ border: 1px solid #ccc;
+ border-collapse: collapse;
+
+ th {
+ text-align: left;
+ padding: 8px;
+ }
+
+ th,
+ td {
+ border: 1px solid #ccc;
+ padding: 8px;
+ }
+ }
+
+ code {
+ background-color: #f8f8f8;
+ border: 1px solid #ccc;
+ padding: 2px 4px;
+ border-radius: 4px;
+ }
+
+ pre {
+ background-color: #f8f8f8;
+ border: 1px solid #ccc;
+ padding: 8px;
+ border-radius: 4px;
+
+ code {
+ border: unset;
+ padding: unset;
+ }
+ }
+}
diff --git a/static/css/notes2.css b/static/css/notes2.css
index b6b0963..71737dd 100644
--- a/static/css/notes2.css
+++ b/static/css/notes2.css
@@ -1,264 +1,390 @@
+@import "theme.css";
+
+:root {
+ --content-width: 900px;
+ --thumbnail-width: 300px;
+ --thumbnail-height: 100px;
+}
+
html {
- background-color: #fff;
+ background-color: #fff;
}
+
#notes2 {
- min-height: 100vh;
- display: grid;
- grid-template-areas: "tree crumbs" "tree sync" "tree name" "tree content" "tree blank";
- grid-template-columns: min-content 1fr;
- grid-template-rows: 48px 56px 48px min-content 1fr;
-}
-@media only screen and (max-width: 600px) {
- #notes2 {
- grid-template-areas: "crumbs" "sync" "name" "content" "blank";
- grid-template-columns: 1fr;
- }
- #notes2 #tree {
- display: none;
- }
+ min-height: 100vh;
+
+ display: grid;
+ grid-template-areas:
+ "tree hum crumbs crumbs ding"
+ "tree hum name name ding"
+ "tree hum sync functions ding"
+ "tree hum content content ding"
+ "tree hum blank blank ding"
+ ;
+ grid-template-columns: min-content minmax(16px, 1fr) minmax(min-content, 820px) 80px minmax(16px, 1fr);
+ grid-template-rows:
+ min-content min-content 48px 1fr;
+
+
+ @media only screen and (max-width: 600px) {
+ grid-template-areas:
+ "crumbs"
+ "sync"
+ "name"
+ "content"
+ "blank"
+ ;
+ grid-template-columns: 1fr;
+
+ #tree {
+ display: none;
+ }
+
+ n2-syncprogress {
+ .el-count {
+ top: 4px;
+ }
+ }
+ }
+
}
+
#tree {
- grid-area: tree;
- padding: 16px 32px;
- background-color: #333;
- color: #ddd;
- z-index: 100;
- border-left: 2px solid #333;
+ grid-area: tree;
+ display: grid;
+ background-color: #fafafa;
+ color: #444;
+ z-index: 100;
+
+ border-right: 1px solid #ddd;
+
+ n2-tree {
+ /*border: 2px solid #f8f8f8;*/
+ padding: 16px 48px 16px 24px;
+ }
+
+ &:focus-within {
+ n2-tree {
+ /*
+ border: 2px solid #fe5f55;
+ */
+ }
+
+ }
+
+
+ #logo {
+ display: grid;
+ position: relative;
+ justify-items: center;
+ margin-top: 8px;
+ margin-bottom: 8px;
+ margin-left: 24px;
+ margin-right: 24px;
+ cursor: pointer;
+
+ img {
+ width: 128px;
+ left: -20px;
+
+ }
+ }
+
+ .icons {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 32px;
+ gap: 8px;
+ }
+
+ .node {
+ display: grid;
+ grid-template-columns: 40px min-content;
+ grid-template-rows:
+ min-content 1fr;
+ margin-top: 12px;
+ align-items: center;
+
+ .expand-toggle {
+ user-select: none;
+ cursor: pointer;
+ justify-self: center;
+
+ img {
+ width: auto;
+ height: 18px;
+ }
+ }
+
+ .name {
+ white-space: nowrap;
+ cursor: pointer;
+ user-select: none;
+
+ &:hover {
+ color: var(--color1);
+ }
+
+ &.selected {
+ color: var(--color1);
+ font-weight: bold;
+ }
+
+ }
+
+ .children {
+ padding-left: 24px;
+ margin-left: 18px;
+ border-left: 1px solid #ddd;
+ grid-column: 1 / -1;
+
+ &.collapsed {
+ display: none;
+ }
+ }
+ }
}
-#tree:focus {
- border-left: 2px solid #FE5F55;
-}
-#tree #logo {
- display: grid;
- position: relative;
- justify-items: center;
- margin-bottom: 8px;
- margin-left: 24px;
- margin-right: 24px;
-}
-#tree #logo img {
- width: 128px;
- left: -20px;
-}
-#tree .icons {
- display: flex;
- justify-content: center;
- margin-bottom: 32px;
- gap: 8px;
-}
-#tree .node {
- display: grid;
- grid-template-columns: 24px min-content;
- grid-template-rows: min-content 1fr;
- margin-top: 12px;
-}
-#tree .node .expand-toggle {
- user-select: none;
-}
-#tree .node .expand-toggle img {
- width: 16px;
- height: 16px;
-}
-#tree .node .name {
- white-space: nowrap;
- cursor: pointer;
- user-select: none;
-}
-#tree .node .name:hover {
- color: #fe5f55;
-}
-#tree .node .name.selected {
- color: #fe5f55;
- font-weight: bold;
-}
-#tree .node .children {
- padding-left: 24px;
- margin-left: 8px;
- border-left: 1px solid #444;
- grid-column: 1 / -1;
-}
-#tree .node .children.collapsed {
- display: none;
+
+#tree-nodes {
+ padding: 16px 32px;
+ /*
+ border-radius: 8px;
+*/
+ /*
+ box-shadow: 5px 5px 10px -5px rgba(0, 0, 0, 0.75);
+ */
}
+
#crumbs {
- grid-area: crumbs;
- display: grid;
- align-items: start;
- justify-items: center;
- margin: 0px 16px;
+ grid-area: crumbs;
+ display: grid;
+ align-items: start;
+ justify-items: center;
+ height: min-content;
+ margin: 0 16px 16px 16px;
+
+ n2-crumbs {
+ background: #e4e4e4;
+ display: flex;
+ flex-wrap: wrap;
+ padding: 8px 16px;
+ background: #e4e4e4;
+ color: #333;
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+
+ &.node-modified {
+ background-color: var(--color1);
+ color: var(--color2);
+
+ .crumb:after {
+ color: var(--color2);
+ }
+ }
+
+ n2-crumb {
+ margin-right: 8px;
+ cursor: pointer;
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+
+ a {
+ text-decoration: none;
+ color: inherit;
+ }
+ }
+
+ n2-crumb:after {
+ content: ">";
+ font-weight: bold;
+ color: var(--color1)
+ }
+
+ n2-crumb:last-child {
+ margin-right: 0;
+ }
+
+ n2-crumb:last-child:after {
+ content: '';
+ margin-left: 0px;
+ }
+
+ }
+
}
-#crumbs .crumbs {
- display: flex;
- flex-wrap: wrap;
- padding: 8px 16px;
- background: #e4e4e4;
- color: #333;
- border-bottom-left-radius: 5px;
- border-bottom-right-radius: 5px;
-}
-#crumbs .crumbs.node-modified {
- background-color: #fe5f55;
- color: #efede8;
-}
-#crumbs .crumbs.node-modified .crumb:after {
- color: #efede8;
-}
-#crumbs .crumbs .crumb {
- margin-right: 8px;
- cursor: pointer;
- user-select: none;
- -webkit-tap-highlight-color: transparent;
-}
-#crumbs .crumbs .crumb:after {
- content: "•";
- margin-left: 8px;
- color: #fe5f55;
-}
-#crumbs .crumbs .crumb:last-child {
- margin-right: 0;
-}
-#crumbs .crumbs .crumb:last-child:after {
- content: '';
- margin-left: 0px;
-}
-#sync-progress {
- grid-area: sync;
- display: grid;
- justify-items: center;
- width: 100%;
- height: 56px;
- position: relative;
-}
-#sync-progress progress {
- width: 100%;
- padding: 0 7px;
- max-width: 900px;
- height: 16px;
- border-radius: 4px;
-}
-#sync-progress progress[value]::-webkit-progress-bar {
- background-color: #eee;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
- border-radius: 4px;
-}
-#sync-progress progress[value]::-moz-progress-bar {
- background-color: #eee;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
- border-radius: 4px;
-}
-#sync-progress progress[value]::-webkit-progress-value {
- background: #ba5f59;
- background: linear-gradient(180deg, #ba5f59 0%, #fe5f55 50%, #ba5f59 100%);
- border-radius: 4px;
-}
-#sync-progress progress[value]::-moz-progress-value {
- background: #ba5f59;
- background: linear-gradient(180deg, #ba5f59 0%, #fe5f55 50%, #ba5f59 100%);
- border-radius: 4px;
-}
-#sync-progress .count {
- width: min-content;
- white-space: nowrap;
- margin-top: 0px;
- color: #888;
- position: absolute;
- top: 22px;
-}
-#sync-progress.hidden {
- visibility: hidden;
- opacity: 0;
- transition: visibility 0s 500ms, opacity 500ms linear;
-}
-#name {
- color: #333;
- font-weight: bold;
- text-align: center;
- font-size: 1.15em;
- margin-top: 0px;
- margin-bottom: 16px;
-}
-/* ============================================================= *
- * Textarea replicates the height of an element expanding height *
- * ============================================================= */
-.grow-wrap {
- /* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */
- display: grid;
- grid-area: content;
- font-size: 1em;
-}
-.grow-wrap::after {
- /* Note the weird space! Needed to preventy jumpy behavior */
- content: attr(data-replicated-value) " ";
- /* This is how textarea text behaves */
- width: calc(100% - 32px);
- max-width: 900px;
- white-space: pre-wrap;
- word-wrap: break-word;
- background: rgba(0, 255, 255, 0.5);
- justify-self: center;
- /* Hidden from view, clicks, and screen readers */
- visibility: hidden;
-}
-.grow-wrap > textarea {
- /* You could leave this, but after a user resizes, then it ruins the auto sizing */
- resize: none;
- /* Firefox shows scrollbar on growth, you can hide like this. */
- overflow: hidden;
-}
-.grow-wrap > textarea,
-.grow-wrap::after {
- /* Identical styling required!! */
- padding: 0.5rem;
- font: inherit;
- /* Place on top of each other */
- grid-area: 1 / 1 / 2 / 2;
+
+n2-syncprogress {
+ --radius: 8px;
+
+ display: grid;
+ grid-area: sync;
+ display: grid;
+ justify-items: center;
+ align-items: center;
+
+ position: relative;
+
+ opacity: 0;
+ transition: height 0s 500ms, opacity 500ms linear, visibility 0s 500ms;
+
+ &.show {
+ opacity: 1;
+ transition: visibility, height 0s, opacity 500ms linear;
+ }
+
+ progress {
+ width: 100%;
+ height: 24px;
+ border-radius: 8px;
+ }
+
+ .count {
+ position: absolute;
+ top: 16px;
+ width: 100%;
+ white-space: nowrap;
+ color: #888;
+ text-align: center;
+ font-size: 12pt;
+ font-weight: bold;
+ }
+
+ progress[value]::-webkit-progress-bar {
+ background-color: #eee;
+ box-shadow: 0 2px var(--radius) rgba(0, 0, 0, 0.25) inset;
+ border-radius: var(--radius);
+ }
+
+ progress[value]::-moz-progress-bar {
+ background-color: #eee;
+ box-shadow: 0 2px var(--radius) rgba(0, 0, 0, 0.25) inset;
+ border-radius: var(--radius);
+ }
+
+ progress[value]::-webkit-progress-value {
+ background: rgb(186, 95, 89);
+ background: linear-gradient(180deg, rgba(186, 95, 89, 1) 0%, rgba(254, 95, 85, 1) 50%, rgba(186, 95, 89, 1) 100%);
+ border-radius: var(--radius);
+ }
+
+ progress[value]::-moz-progress-value {
+ background: rgb(186, 95, 89);
+ background: linear-gradient(180deg, rgba(186, 95, 89, 1) 0%, rgba(254, 95, 85, 1) 50%, rgba(186, 95, 89, 1) 100%);
+ border-radius: var(--radius);
+ }
+
}
+
/* ============================================================= */
-#node-content {
- justify-self: center;
- word-wrap: break-word;
- font-family: monospace;
- color: #333;
- width: calc(100% - 32px);
- max-width: 900px;
- resize: none;
- border: none;
- outline: none;
-}
-#node-content:invalid {
- background: #f5f5f5;
- padding-top: 16px;
+
+n2-nodeui {
+ margin-bottom: 32px;
+
+ .el-name {
+ grid-area: name;
+ color: #333;
+ font-weight: bold;
+ text-align: center;
+ font-size: 1.15em;
+ margin-top: 8px;
+ margin-bottom: 0px;
+ }
+
+ .el-functions {
+ grid-area: functions;
+ }
+
+ .el-node-content {
+ grid-area: content;
+ justify-self: center;
+ word-wrap: break-word;
+ font-family: monospace;
+ color: #333;
+
+ width: 100%;
+ max-width: var(--content-width);
+ field-sizing: content;
+
+ resize: none;
+ outline: none;
+
+ padding: 32px 0;
+ border-left: none;
+ border-right: none;
+ border-top: 1px solid #e0e0e0;
+ border-bottom: 1px solid #e0e0e0;
+ margin-bottom: 32px;
+
+ &:invalid {
+ background: #f5f5f5;
+ padding-top: 16px;
+ }
+ }
+
+ .el-node-markdown {
+ grid-area: content;
+ display: none;
+
+ border-top: 1px solid #e0e0e0;
+ border-bottom: 1px solid #e0e0e0;
+ margin-bottom: 32px;
+ }
+
+ &.show-markdown {
+ .el-node-content {
+ display: none;
+ }
+
+ .el-node-markdown {
+ display: block;
+ }
+ }
}
+
#blank {
- grid-area: blank;
- height: 32px;
+ grid-area: blank;
+ height: 32px;
}
-dialog.op::backdrop {
- background: rgba(0, 0, 0, 0.5);
+
+dialog.op {
+ &::backdrop {
+ background: rgba(0, 0, 0, 0.5);
+ }
+
+ .header {
+ font-weight: bold;
+ margin-top: 16px;
+
+ &:first-child {
+ margin-top: 0px;
+ }
+ }
+
}
-dialog.op .header {
- font-weight: bold;
- margin-top: 16px;
-}
-dialog.op .header:first-child {
- margin-top: 0px;
-}
-#op-search .results {
- display: grid;
- grid-template-columns: min-content min-content;
- grid-gap: 6px 16px;
-}
-#op-search .results div {
- white-space: nowrap;
-}
-#op-search .results .ancestors {
- display: flex;
-}
-#op-search .results .ancestors .ancestor::after {
- content: ">";
- margin: 0px 8px;
- color: #a00;
-}
-#op-search .results .ancestors .ancestor:last-child::after {
- content: "";
+
+#op-search {
+ .results {
+ display: grid;
+ grid-template-columns: min-content min-content;
+ grid-gap: 6px 16px;
+
+ div {
+ white-space: nowrap;
+ }
+
+
+ .ancestors {
+ display: flex;
+
+ .ancestor::after {
+ content: ">";
+ margin: 0px 8px;
+ color: #a00;
+ }
+
+ .ancestor:last-child::after {
+ content: "";
+ }
+ }
+ }
}
diff --git a/static/css/theme.css b/static/css/theme.css
index e69de29..b9c47ed 100644
--- a/static/css/theme.css
+++ b/static/css/theme.css
@@ -0,0 +1,5 @@
+:root {
+ --color1: #fe5f55;
+ --color2: #efede8;
+ --color3: #666;
+}
diff --git a/static/images/collapsed.svg b/static/images/collapsed.svg
index 8bd376f..d93f4ca 100644
--- a/static/images/collapsed.svg
+++ b/static/images/collapsed.svg
@@ -2,73 +2,49 @@
+ transform="translate(-102.39375,-146.31458)">
+
folder-outline
+
+
+
diff --git a/static/images/expanded.svg b/static/images/expanded.svg
index e1a6f66..017e8a4 100644
--- a/static/images/expanded.svg
+++ b/static/images/expanded.svg
@@ -2,64 +2,43 @@
+ transform="translate(-101.33542,-147.10833)">folder-openfolder-open-outline
diff --git a/static/images/icon_markdown.svg b/static/images/icon_markdown.svg
new file mode 100644
index 0000000..f8d0aae
--- /dev/null
+++ b/static/images/icon_markdown.svg
@@ -0,0 +1,57 @@
+
+
+
+
diff --git a/static/images/icon_markdown_hollow.svg b/static/images/icon_markdown_hollow.svg
new file mode 100644
index 0000000..d938c6f
--- /dev/null
+++ b/static/images/icon_markdown_hollow.svg
@@ -0,0 +1,50 @@
+
+
diff --git a/static/images/icon_refresh.svg b/static/images/icon_refresh.svg
index d46322e..a6aa907 100644
--- a/static/images/icon_refresh.svg
+++ b/static/images/icon_refresh.svg
@@ -7,7 +7,7 @@
viewBox="0 0 4.2333398 5.8208399"
version="1.1"
id="svg1"
- inkscape:version="1.3.2 (091e20e, 2023-11-25)"
+ inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
sodipodi:docname="icon_refresh.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
@@ -23,15 +23,16 @@
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
- inkscape:zoom="0.83651094"
- inkscape:cx="7.7703706"
- inkscape:cy="11.356695"
+ inkscape:zoom="23.548693"
+ inkscape:cx="6.9218279"
+ inkscape:cy="12.5697"
inkscape:window-width="1916"
inkscape:window-height="1161"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
- inkscape:current-layer="layer1" />
+ inkscape:current-layer="layer1"
+ showgrid="false" />
+ style="stroke-width:0.264583;fill:#fe5f55;fill-opacity:1" />
diff --git a/static/images/icon_save.svg b/static/images/icon_save.svg
new file mode 100644
index 0000000..0846a73
--- /dev/null
+++ b/static/images/icon_save.svg
@@ -0,0 +1,49 @@
+
+
+
+
diff --git a/static/images/icon_save_disabled.svg b/static/images/icon_save_disabled.svg
new file mode 100644
index 0000000..907cee6
--- /dev/null
+++ b/static/images/icon_save_disabled.svg
@@ -0,0 +1,49 @@
+
+
+
+
diff --git a/static/images/icon_search.svg b/static/images/icon_search.svg
index 8be3977..6de83dd 100644
--- a/static/images/icon_search.svg
+++ b/static/images/icon_search.svg
@@ -7,7 +7,7 @@
viewBox="0 0 109.40056 109.39984"
version="1.1"
id="svg8"
- inkscape:version="1.4 (e7c3feb, 2024-10-09)"
+ inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
sodipodi:docname="icon_search.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
@@ -27,16 +27,16 @@
inkscape:pageshadow="2"
inkscape:zoom="0.70710678"
inkscape:cx="206.47518"
- inkscape:cy="207.18229"
+ inkscape:cy="207.88939"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
- inkscape:window-width="2190"
- inkscape:window-height="1404"
- inkscape:window-x="1463"
- inkscape:window-y="16"
- inkscape:window-maximized="0"
+ inkscape:window-width="1916"
+ inkscape:window-height="1161"
+ inkscape:window-x="0"
+ inkscape:window-y="18"
+ inkscape:window-maximized="1"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
@@ -62,6 +62,6 @@
+ style="stroke-width:6.25145;fill:#fe5f55;fill-opacity:1" />
diff --git a/static/images/leaf.svg b/static/images/leaf.svg
index ed44541..306a2a0 100644
--- a/static/images/leaf.svg
+++ b/static/images/leaf.svg
@@ -2,56 +2,51 @@
+ transform="translate(-107.95,-148.16667)">folder-openfolder-open-outlinenotebook-outlinetext-box-outline
diff --git a/static/js/app.mjs b/static/js/app.mjs
new file mode 100644
index 0000000..7fa4dda
--- /dev/null
+++ b/static/js/app.mjs
@@ -0,0 +1,331 @@
+import { ROOT_NODE } from 'node_store'
+import { CustomHTMLElement } from './lib/custom_html_element.mjs'
+import { N2Tree } from 'tree'
+import { Node } from 'node'
+
+export class App {
+ constructor() {// {{{
+ this.currentNode = null
+ this.tree = new N2Tree()
+ this.crumbs = new N2Crumbs()
+ this.crumbsElement = document.getElementById('crumbs')
+ this.nodeUI = document.getElementById('note')
+
+ _mbus.subscribe('TREE_TRUNK_FETCHED', async () => {
+ document.getElementById('tree').append(this.tree.render())
+ document.getElementById('tree-nodes')?.focus()
+
+ const startNode = await this.getStartNode()
+ this.goToNode(startNode.UUID, false, false)
+ })
+
+ _mbus.subscribe('TREE_NODE_SELECTED', event => {
+ const node = event.detail.data
+ this.goToNode(node.UUID, false, false)
+ })
+
+ _mbus.subscribe('GO_TO_NODE', event => {
+ const node = event.detail.data
+ this.goToNode(node.nodeUUID, node.dontPush, node.dontExpand)
+ })
+
+ window.addEventListener('keydown', event => this.keyHandler(event))
+ window.addEventListener('popstate', event => this.popState(event))
+ document.getElementById('notes2').addEventListener('click', event => {
+ if (event.target.id === 'notes2')
+ document.getElementById('node-content')?.focus()
+ })
+
+ window._sync = new Sync()
+
+ // I think it is uncomfortable having the sync running as soon as the page load.
+ // I haven't gotten the time to look at the page before stuff jumps around.
+ // There a slight delay to initiate sync seems reasonable.
+ setTimeout(() => window._sync.run(), 1000)
+ }// }}}
+
+ keyHandler(event) {//{{{
+ let handled = true
+
+ // All keybindings is Alt+Shift, since the popular browsers at the time (2023) allows to override thees.
+ // Ctrl+S is the exception to using Alt+Shift, since it is overridable and in such widespread use for saving.
+ // Thus, the exception is acceptable to consequent use of alt+shift.
+ if (!(event.shiftKey && event.altKey) && !(event.key.toUpperCase() === 'S' && event.ctrlKey))
+ return
+
+ switch (event.key.toUpperCase()) {
+ case 'T':
+ if (document.activeElement.id === 'tree-nodes') {
+ console.log('take focus')
+ this.nodeUI.takeFocus()
+ } else {
+ this.tree.focus()
+ }
+ break
+
+ case 'F':
+ _mbus.dispatch('op-search')
+ break
+ /*
+ case 'C':
+ this.showPage('node')
+ break
+
+ case 'E':
+ this.showPage('keys')
+ break
+ */
+
+ case 'M':
+ globalThis._mbus.dispatch('MARKDOWN_TOGGLE')
+ break
+
+ case 'N':
+ this.createNode()
+ break
+
+ /*
+ case 'P':
+ this.showPage('node-properties')
+ break
+
+ */
+ case 'S':
+ this.saveNode()
+ /*
+ else if (this.page.value === 'node-properties')
+ this.nodeProperties.current.save()
+ */
+ break
+ /*
+
+ case 'U':
+ this.showPage('upload')
+ break
+
+ case 'F':
+ this.showPage('search')
+ break
+ */
+
+ default:
+ handled = false
+ }
+
+ if (handled) {
+ event.preventDefault()
+ event.stopPropagation()
+ }
+ }//}}}
+ popState(event) {// {{{
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: event.state.nodeUUID, dontPush: true, dontExpand: true })
+ }// }}}
+ async getStartNode() {//{{{
+ let nodeUUID = ROOT_NODE
+
+ // Is a UUID provided on the URI as an anchor?
+ const parts = document.URL.split('#')
+ if (parts[1]?.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i))
+ nodeUUID = parts[1]
+
+ return await nodeStore.get(nodeUUID)
+ }//}}}
+ async saveNode() {//{{{
+ if (!this.currentNode.isModified())
+ return
+
+ /* The node history is a local store for node history.
+ * This could be provisioned from the server or cleared if
+ * deemed unnecessary.
+ *
+ * The send queue is what will be sent back to the server
+ * to have a recorded history of the notes.
+ *
+ * A setting to be implemented in the future could be to
+ * not save the history locally at all. */
+ const node = this.currentNode
+
+ // The node is still in its old state and will present
+ // the unmodified content to the node store.
+ const history = nodeStore.nodesHistory.add(node)
+
+ // Prepares the node object for saving.
+ // Sets Updated value to current date and time.
+ await node.save()
+
+ // Updated node is added to the send queue to be stored on server.
+ const sendQueue = nodeStore.sendQueue.add(node)
+
+ // Updated node is saved to the primary node store.
+ const nodeStoreAdding = nodeStore.add([node])
+
+ await Promise.all([history, sendQueue, nodeStoreAdding])
+ }//}}}
+ async createNode() {//{{{
+ let name = prompt("Name")
+ if (!name)
+ return
+
+ const nn = Node.create(name, this.currentNode.UUID)
+ nn.save()
+
+ nodeStore.sendQueue.add(nn)
+ nodeStore.add([nn])
+
+ }//}}}
+ async goToNode(nodeUUID, dontPush, dontExpand) {//{{{
+ if (nodeUUID === null || nodeUUID === undefined)
+ return
+
+ // Don't switch notes until saved.
+ if (this.nodeUI.isModified()) {
+ if (!confirm("Changes not saved. Do you want to discard changes?"))
+ return
+ }
+
+ if (!dontPush)
+ history.pushState({ nodeUUID }, '', `/notes2#${nodeUUID}`)
+
+ const node = nodeStore.node(nodeUUID)
+ node.reset() // any modifications are discarded.
+
+ this.currentNode = node
+ this.tree.setSelected(node, dontExpand)
+
+ const ancestors = await nodeStore.getNodeAncestry(node)
+ _mbus.dispatch('CRUMBS_SET', ancestors, () => this.crumbsElement.replaceChildren(this.crumbs.render()))
+ _mbus.dispatch('NODE_UI_OPEN', node)
+ _mbus.dispatch('NODE_UNMODIFIED')
+
+ // Scrolls node into view.
+ this.tree.makeVisible(node)
+ }//}}}
+}
+
+class N2Crumbs extends CustomHTMLElement {
+ static {// {{{
+ this.tmpl = document.createElement('template')
+ this.tmpl.innerHTML = `
+ `
+ }// }}}
+ constructor() {// {{{
+ super()
+ this.classList.add('crumbs')
+
+ this.crumbs = []
+
+ _mbus.subscribe('CRUMBS_SET', event => {
+ this.crumbs = event.detail.data
+ })
+ }// }}}
+ render() {// {{{
+ const crumbs = this.crumbs.map(node =>
+ new N2Crumb(
+ node.get('Name'),
+ node.UUID,
+ )
+ )
+
+ const start = new N2Crumb('Start', ROOT_NODE)
+ crumbs.push(start)
+
+ this.replaceChildren(...crumbs.reverse())
+ return this
+ }// }}}
+}
+customElements.define('n2-crumbs', N2Crumbs)
+
+class N2Crumb extends CustomHTMLElement {
+ static {// {{{
+ this.tmpl = document.createElement('template')
+ this.tmpl.innerHTML = `
+
+ `
+ }// }}}
+ constructor(label, uuid) {// {{{
+ super()
+ this.classList.add('crumb')
+
+ this.label = label
+ this.uuid = uuid
+
+ this.elLink.href = `/notes2#${this.uuid}`
+ this.elLink.innerText = this.label
+ this.elLink.addEventListener('click', () => _mbus.dispatch("GO_TO_NODE", { nodeUUID: this.uuid, dontPush: false, dontExpand: true }))
+ }// }}}
+}
+customElements.define('n2-crumb', N2Crumb)
+
+function tmpl(html) {// {{{
+ const el = document.createElement('template')
+ el.innerHTML = html
+ return el.content.children
+}// }}}
+
+class Op {
+ constructor(id) {// {{{
+ this.id = id
+ _mbus.subscribe(this.id, p => this.render(p))
+ }// }}}
+ render(html) {// {{{
+ const op = document.getElementById('op')
+ const t = document.createElement('template')
+ t.innerHTML = ``
+ op.replaceChildren(t.content)
+ document.getElementById(this.id).showModal()
+ }// }}}
+ get(selector) {// {{{
+ return document.querySelector(`#${this.id} ${selector}`)
+ }// }}}
+ bind(selector, event, fn) {// {{{
+ this.get(selector).addEventListener(event, evt => fn(evt))
+ }// }}}
+}
+
+class OpSearch extends Op {
+ constructor() {// {{{
+ super('op-search')
+ }// }}}
+ render() {// {{{
+ super.render(`
+
+
+
+
+
+
+ `)
+
+ this.bind('input[type="text"]', 'keydown', evt => this.search(evt))
+ }// }}}
+ search(event) {// {{{
+ if (event.key !== 'Enter')
+ return
+
+ const searchFor = document.querySelector('#op-search input').value
+ nodeStore.search(searchFor, ROOT_NODE)
+ .then(res => this.displayResults(res))
+ }// }}}
+ displayResults(results) {// {{{
+ const rs = []
+ for (const r of results) {
+ const ancestors = r.ancestry.reverse().map(a => {
+ const div = tmpl(`${a.data.Name}
`)
+ div[0].addEventListener('click', () => _notes2.current.goToNode(a.UUID))
+ return div[0]
+ })
+
+
+ const div = tmpl(`${r.name}
`)
+ div[0].addEventListener('click', () => _notes2.current.goToNode(r.uuid))
+ rs.push(...div)
+
+ const ancDev = tmpl('')
+ ancDev[0].append(...ancestors)
+ rs.push(ancDev[0])
+ }
+ this.get('.results').replaceChildren(...rs)
+ }// }}}
+}
+
+// vim: foldmethod=marker
diff --git a/static/js/lib/custom_html_element.mjs b/static/js/lib/custom_html_element.mjs
new file mode 100644
index 0000000..dedb5d8
--- /dev/null
+++ b/static/js/lib/custom_html_element.mjs
@@ -0,0 +1,57 @@
+export class CustomHTMLElement extends HTMLElement {
+ constructor() {// {{{
+ super()
+
+ this.appendChild(this.constructor.tmpl.content.cloneNode(true))
+
+ this.querySelectorAll('*').forEach(el => {
+ const field = el.dataset.field
+ if (field !== undefined) {
+ const fieldName = this.toElementName('field', field)
+ this[fieldName] = el
+ }
+
+ const name = el.dataset.el
+ if (name !== undefined) {
+ const elName = this.toElementName('el', name)
+ this[elName] = el
+ el.classList.add('el-' + name)
+ }
+ })
+ }// }}}
+ toElementName(prefix, str) {// {{{
+ str = prefix + '-' + str
+ return str.replace(/-(id|[a-z])/g, match => match.toUpperCase().replace('-', ''))
+ }// }}}
+}
+
+export class StupidPreactCustomHTMLElement extends HTMLElement {
+ constructor() {// {{{
+ super()
+
+ // Stupid stuff because of Preact.
+ this.clonedNodes = this.constructor.tmpl.content.cloneNode(true)
+ this.clonedNodes.querySelectorAll('*').forEach(el => {
+ const field = el.dataset.field
+ if (field !== undefined) {
+ const fieldName = this.toElementName('field', field)
+ this[fieldName] = el
+ }
+
+ const name = el.dataset.el
+ if (name !== undefined) {
+ const elName = this.toElementName('el', name)
+ this[elName] = el
+ el.classList.add('el-' + name)
+ }
+ })
+ }// }}}
+ toElementName(prefix, str) {// {{{
+ str = prefix + '-' + str
+ return str.replace(/-(id|[a-z])/g, match => match.toUpperCase().replace('-', ''))
+ }// }}}
+ connectedCallback() {// {{{
+ // Stupid stuff because of Preact.
+ this.appendChild(this.clonedNodes)
+ }// }}}
+}
diff --git a/static/js/lib/node_modules/.package-lock.json b/static/js/lib/node_modules/.package-lock.json
index 441fdf4..3d3d164 100644
--- a/static/js/lib/node_modules/.package-lock.json
+++ b/static/js/lib/node_modules/.package-lock.json
@@ -4,14 +4,24 @@
"requires": true,
"packages": {
"node_modules/marked": {
- "version": "11.1.1",
- "resolved": "https://registry.npmjs.org/marked/-/marked-11.1.1.tgz",
- "integrity": "sha512-EgxRjgK9axsQuUa/oKMx5DEY8oXpKJfk61rT5iY3aRlgU6QJtUcxU5OAymdhCvWvhYcd9FKmO5eQoX8m9VGJXg==",
+ "version": "18.0.3",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.3.tgz",
+ "integrity": "sha512-7VT90JOkDeaRWpfjOReRGPEKn0ecdARBkDGL+tT1wZY0efPPqkUxLUSmzy/C7TIylQYJC9STISEsCHrqb/7VIA==",
+ "license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
- "node": ">= 18"
+ "node": ">= 20"
+ }
+ },
+ "node_modules/marked-token-position": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/marked-token-position/-/marked-token-position-2.0.2.tgz",
+ "integrity": "sha512-IMyr4mR3A5uFReXn7cxLDgDLjefG110ANy0oMGs5+gB7NsdIbv9YoVoJuGxuMSFHWOeIFkAzjdSoFNVKcMPfZw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "marked": ">=16.2.0 <19"
}
},
"node_modules/preact": {
diff --git a/static/js/lib/node_modules/marked-token-position/LICENSE b/static/js/lib/node_modules/marked-token-position/LICENSE
new file mode 100644
index 0000000..5d36390
--- /dev/null
+++ b/static/js/lib/node_modules/marked-token-position/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 @UziTech
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/static/js/lib/node_modules/marked-token-position/README.md b/static/js/lib/node_modules/marked-token-position/README.md
new file mode 100644
index 0000000..a10f43c
--- /dev/null
+++ b/static/js/lib/node_modules/marked-token-position/README.md
@@ -0,0 +1,160 @@
+# marked-token-position
+
+Add `position` field for each token.
+
+```ts
+interface Position {
+ /**
+ * Positions for each line of the token. LinePositions will not include the newline character for the line.
+ */
+ lines: LinePosition[]
+ /**
+ * Position at the beginning of token
+ */
+ start: PositionFields;
+ /**
+ * Position at the end of token
+ */
+ end: PositionFields;
+}
+
+interface LinePosition {
+ /**
+ * Position at the beginning of line
+ */
+ start: PositionFields;
+ /**
+ * Position at the end of line. Will not include the newline character.
+ */
+ end: PositionFields;
+}
+
+interface PositionFields {
+ /**
+ * Number of characters from the beginning of the markdown string
+ */
+ offset: number;
+ /**
+ * Line number of the token. Starts at line 0.
+ */
+ line: number;
+ /**
+ * Column number of the token. Starts at column 0.
+ */
+ column: number;
+}
+```
+
+# Usage
+
+## Extension
+
+```js
+import {Marked} from "marked";
+import markedTokenPosition from "marked-token-position";
+
+// or UMD script
+//
+//
+// const Marked = marked.Marked;
+
+const marked = new Marked();
+
+function anotherExtension {
+ return {
+ walkTokens(token) {
+ // token has `position` field
+ }
+ hooks: {
+ processAllTokens(tokens) {
+ // tokens have `position` field
+ }
+ }
+ };
+}
+
+marked.use(anotherExtension(), markedTokenPosition());
+
+marked.parse("# example markdown");
+```
+
+The `position` field will be added to the tokens so any other extension can
+use the `position` field in a `walkTokens` function or `processAllTokens` hook.
+
+> [!CAUTION]
+> The `processAllTokens` hook is used by this extension so any other extension
+> using `processAllTokens` that requires the `position` field must be added
+> before this extension because marked calls the `processAllTokens` hooks in
+> reverse order.
+
+The tokens will look like:
+
+```json
+[
+ {
+ "type": "heading",
+ "raw": "# example markdown",
+ "depth": 1,
+ "text": "example markdown",
+ "tokens": [
+ {
+ "type": "text",
+ "raw": "example markdown",
+ "text": "example markdown",
+ "escaped": false,
+ "position": {
+ "start": {
+ "offset": 2,
+ "line": 0,
+ "column": 2
+ },
+ "end": {
+ "offset": 18,
+ "line": 0,
+ "column": 18
+ }
+ }
+ }
+ ],
+ "position": {
+ "start": {
+ "offset": 0,
+ "line": 0,
+ "column": 0
+ },
+ "end": {
+ "offset": 18,
+ "line": 0,
+ "column": 18
+ }
+ }
+ }
+]
+```
+
+## addTokenPositions
+
+Calling `marked.lexer()` will not add the `position` field with the extension
+since the extension is only called on `marked.parse()` and `marked.parseInline()`.
+
+An `addTokenPositions` function is exported to add the `position` field to the
+tokens returned by `marked.lexer()`.
+
+```js
+import {Marked} from "marked";
+import {addTokenPositions} from "marked-token-position";
+
+// or UMD script
+//
+//
+// const Marked = marked.Marked;
+// const addTokenPositions = markedTokenPosition.addTokenPositions;
+
+
+const marked = new Marked();
+const tokens = marked.lexer("# example markdown");
+
+addTokenPositions(tokens);
+
+// tokens now have a `position` field
+```
diff --git a/static/js/lib/node_modules/marked-token-position/lib/index.d.ts b/static/js/lib/node_modules/marked-token-position/lib/index.d.ts
new file mode 100644
index 0000000..f61cd65
--- /dev/null
+++ b/static/js/lib/node_modules/marked-token-position/lib/index.d.ts
@@ -0,0 +1,59 @@
+// Generated by dts-bundle-generator v9.5.1
+
+import { MarkedExtension, Token, Tokens } from 'marked';
+
+export interface TokenWithPosition extends Tokens.Generic {
+ position: Position;
+}
+export interface Position {
+ /**
+ * Positions for each line of the token. LinePositions will not include the newline character for the line.
+ */
+ lines: LinePosition[];
+ /**
+ * Position at the beginning of token
+ */
+ start: PositionFields;
+ /**
+ * Position at the end of token
+ */
+ end: PositionFields;
+}
+export interface LinePosition {
+ /**
+ * Position at the beginning of line
+ */
+ start: PositionFields;
+ /**
+ * Position at the end of line. Will not include the newline character.
+ */
+ end: PositionFields;
+}
+export interface PositionFields {
+ /**
+ * Number of characters from the beginning of the markdown string
+ */
+ offset: number;
+ /**
+ * Line number of the token. Starts at line 0.
+ */
+ line: number;
+ /**
+ * Column number of the token. Starts at column 0.
+ */
+ column: number;
+}
+/**
+ * Add position field to tokens
+ */
+export declare function addTokenPositions(tokens: Token[]): TokenWithPosition[];
+/**
+ * Marked extension to add position field to tokens
+ */
+declare function _default(options?: {}): MarkedExtension;
+
+export {
+ _default as default,
+};
+
+export {};
diff --git a/static/js/lib/node_modules/marked-token-position/lib/index.esm.js b/static/js/lib/node_modules/marked-token-position/lib/index.esm.js
new file mode 100644
index 0000000..41a85f7
--- /dev/null
+++ b/static/js/lib/node_modules/marked-token-position/lib/index.esm.js
@@ -0,0 +1,6 @@
+function g(u){let i=u.map(r=>r.raw).join("");return h(u,0,0,0,i).tokens}function b(u={}){return{hooks:{processAllTokens(i){return g(i)}}}}function h(u,i,r,f,l){for(let s of u){let n=s,a=T(i,r,f,l,n.raw);if(n.position=a,n.tokens&&h(n.tokens,i,r,f,l),n.childTokens){let c=i,t=r,e=f,d=l;for(let k of n.childTokens){let o=h(n[k],c,t,e,d);c=o.offset,t=o.line,e=o.column,d=o.markdown}}if(n.type==="list"&&h(n.items,i,r,f,l),n.type==="table"){let c=i,t=r,e=f,d=l;for(let k of n.header){let o=h(k.tokens,c,t,e,d);c=o.offset,t=o.line,e=o.column,d=o.markdown}for(let k of n.rows)for(let o of k){let P=h(o.tokens,c,t,e,d);c=P.offset,t=P.line,e=P.column,d=P.markdown}}let m=a.end.offset-i;i=a.end.offset,r=a.end.line,f=a.end.column,l=l.slice(m)}return{tokens:u,offset:i,line:r,column:f,markdown:l}}function T(u,i,r,f,l){let s=[],n=l.split(`
+`),a=f.split(`
+`);n:for(let t=0;t<=a.length-n.length;t++){s=[];for(let e=0;e0?`
+`:""),x={offset:u+P.length+o,line:i+t+e,column:(t+e===0?r:0)+o},p={offset:x.offset+k.length,line:x.line,column:x.column+k.length};s.push({start:x,end:p})}break}if(s.length===0)throw new Error(`Cannot find ${JSON.stringify(l)} in ${JSON.stringify(f)}`);let m=s[0].start,c=s.at(-1).end;return s.length>1&&s.at(-1).start.offset===c.offset&&(s=s.slice(0,-1)),{lines:s,start:m,end:c}}export{g as addTokenPositions,b as default};
+//# sourceMappingURL=index.esm.js.map
diff --git a/static/js/lib/node_modules/marked-token-position/lib/index.esm.js.map b/static/js/lib/node_modules/marked-token-position/lib/index.esm.js.map
new file mode 100644
index 0000000..4eb0539
--- /dev/null
+++ b/static/js/lib/node_modules/marked-token-position/lib/index.esm.js.map
@@ -0,0 +1,7 @@
+{
+ "version": 3,
+ "sources": ["../src/index.ts"],
+ "sourcesContent": ["/* node:coverage ignore next */\nimport type { MarkedExtension, Token, Tokens } from 'marked';\n\nexport interface TokenWithPosition extends Tokens.Generic {\n position: Position;\n}\ninterface Position {\n /**\n * Positions for each line of the token. LinePositions will not include the newline character for the line.\n */\n lines: LinePosition[]\n /**\n * Position at the beginning of token\n */\n start: PositionFields;\n /**\n * Position at the end of token\n */\n end: PositionFields;\n}\n\ninterface LinePosition {\n /**\n * Position at the beginning of line\n */\n start: PositionFields;\n /**\n * Position at the end of line. Will not include the newline character.\n */\n end: PositionFields;\n}\n\ninterface PositionFields {\n /**\n * Number of characters from the beginning of the markdown string\n */\n offset: number;\n /**\n * Line number of the token. Starts at line 0.\n */\n line: number;\n /**\n * Column number of the token. Starts at column 0.\n */\n column: number;\n}\n\n/**\n * Add position field to tokens\n */\nexport function addTokenPositions(tokens: Token[]) {\n const markdown = tokens.map(token => token.raw).join('');\n return addPosition(tokens, 0, 0, 0, markdown).tokens;\n}\n\n/**\n * Marked extension to add position field to tokens\n */\nexport default function(options = {}): MarkedExtension {\n return {\n hooks: {\n processAllTokens(tokens) {\n return addTokenPositions(tokens);\n },\n },\n };\n}\n\nfunction addPosition(tokens: Token[], offset: number, line: number, column: number, markdown: string) {\n for (const token of tokens) {\n const genericToken = token as Tokens.Generic;\n const position = getPosition(offset, line, column, markdown, genericToken.raw);\n genericToken.position = position;\n\n if (genericToken.tokens) {\n addPosition(genericToken.tokens, offset, line, column, markdown);\n }\n\n if (genericToken.childTokens) {\n let nextOffset = offset;\n let nextLine = line;\n let nextColumn = column;\n let nextMarkdown = markdown;\n for (const childToken of genericToken.childTokens) {\n const nextPosition = addPosition(genericToken[childToken], nextOffset, nextLine, nextColumn, nextMarkdown);\n nextOffset = nextPosition.offset;\n nextLine = nextPosition.line;\n nextColumn = nextPosition.column;\n nextMarkdown = nextPosition.markdown;\n }\n }\n\n if (genericToken.type === 'list') {\n addPosition(genericToken.items, offset, line, column, markdown);\n }\n\n if (genericToken.type === 'table') {\n let nextOffset = offset;\n let nextLine = line;\n let nextColumn = column;\n let nextMarkdown = markdown;\n for (const headerCell of genericToken.header) {\n const nextPosition = addPosition(headerCell.tokens, nextOffset, nextLine, nextColumn, nextMarkdown);\n nextOffset = nextPosition.offset;\n nextLine = nextPosition.line;\n nextColumn = nextPosition.column;\n nextMarkdown = nextPosition.markdown;\n }\n for (const row of genericToken.rows) {\n for (const rowCell of row) {\n const nextPosition = addPosition(rowCell.tokens, nextOffset, nextLine, nextColumn, nextMarkdown);\n nextOffset = nextPosition.offset;\n nextLine = nextPosition.line;\n nextColumn = nextPosition.column;\n nextMarkdown = nextPosition.markdown;\n }\n }\n }\n\n const deltaOffset = position.end.offset - offset;\n offset = position.end.offset;\n line = position.end.line;\n column = position.end.column;\n markdown = markdown.slice(deltaOffset);\n }\n\n return {\n tokens: tokens as TokenWithPosition[],\n offset,\n line,\n column,\n markdown,\n };\n}\n\nfunction getPosition(offset: number, line: number, column: number, markdown: string, raw: string): Position {\n let lines: LinePosition[] = [];\n const rawLines = raw.split('\\n');\n const markdownLines = markdown.split('\\n');\n\n // eslint-disable-next-line no-labels\n md: for (let i = 0; i <= markdownLines.length - rawLines.length; i++) {\n lines = [];\n for (let j = 0; j < rawLines.length; j++) {\n const markdownLine = markdownLines[i + j];\n const rawLine = rawLines[j];\n const lineStartOffset = markdownLine.indexOf(rawLine);\n\n if (lineStartOffset === -1) {\n // eslint-disable-next-line no-labels\n continue md;\n }\n\n const beforeMarkdownLines = markdownLines.slice(0, i + j).join('\\n') + (i + j > 0 ? '\\n' : '');\n const start = {\n offset: offset + beforeMarkdownLines.length + lineStartOffset,\n line: line + i + j,\n column: (i + j === 0 ? column : 0) + lineStartOffset,\n };\n const end = {\n offset: start.offset + rawLine.length,\n line: start.line,\n column: start.column + rawLine.length,\n };\n\n lines.push({\n start,\n end,\n });\n }\n break;\n }\n\n /* node:coverage ignore next 4 */\n if (lines.length === 0) {\n // This shouldn't ever happen but if it does it would be nice to have a good error message\n throw new Error(`Cannot find ${JSON.stringify(raw)} in ${JSON.stringify(markdown)}`);\n }\n\n const start = lines[0].start;\n const end = lines.at(-1)!.end;\n\n if (lines.length > 1 && lines.at(-1)!.start.offset === end.offset) {\n lines = lines.slice(0, -1);\n }\n\n return {\n lines,\n start,\n end,\n };\n}\n"],
+ "mappings": "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",
+ "names": ["addTokenPositions", "tokens", "markdown", "token", "addPosition", "index_default", "options", "offset", "line", "column", "genericToken", "position", "getPosition", "nextOffset", "nextLine", "nextColumn", "nextMarkdown", "childToken", "nextPosition", "headerCell", "row", "rowCell", "deltaOffset", "raw", "lines", "rawLines", "markdownLines", "md", "i", "j", "markdownLine", "rawLine", "lineStartOffset", "beforeMarkdownLines", "start", "end"]
+}
diff --git a/static/js/lib/node_modules/marked-token-position/lib/index.umd.js b/static/js/lib/node_modules/marked-token-position/lib/index.umd.js
new file mode 100644
index 0000000..253cb7d
--- /dev/null
+++ b/static/js/lib/node_modules/marked-token-position/lib/index.umd.js
@@ -0,0 +1,9 @@
+(function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("markedTokenPosition",f)}else {g["markedTokenPosition"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports};
+var m=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var F=(e,n)=>()=>(e&&(n=e(e=0)),n);var M=(e,n)=>{for(var o in n)m(e,o,{get:n[o],enumerable:!0})},j=(e,n,o,f)=>{if(n&&typeof n=="object"||typeof n=="function")for(let t of y(n))!C.call(e,t)&&t!==o&&m(e,t,{get:()=>n[t],enumerable:!(f=O(n,t))||f.enumerable});return e};var b=e=>j(m({},"__esModule",{value:!0}),e);var T={};M(T,{addTokenPositions:()=>L,default:()=>E});function L(e){let n=e.map(o=>o.raw).join("");return x(e,0,0,0,n).tokens}function E(e={}){return{hooks:{processAllTokens(n){return L(n)}}}}function x(e,n,o,f,t){for(let c of e){let i=c,a=S(n,o,f,t,i.raw);if(i.position=a,i.tokens&&x(i.tokens,n,o,f,t),i.childTokens){let d=n,r=o,s=f,u=t;for(let k of i.childTokens){let l=x(i[k],d,r,s,u);d=l.offset,r=l.line,s=l.column,u=l.markdown}}if(i.type==="list"&&x(i.items,n,o,f,t),i.type==="table"){let d=n,r=o,s=f,u=t;for(let k of i.header){let l=x(k.tokens,d,r,s,u);d=l.offset,r=l.line,s=l.column,u=l.markdown}for(let k of i.rows)for(let l of k){let P=x(l.tokens,d,r,s,u);d=P.offset,r=P.line,s=P.column,u=P.markdown}}let p=a.end.offset-n;n=a.end.offset,o=a.end.line,f=a.end.column,t=t.slice(p)}return{tokens:e,offset:n,line:o,column:f,markdown:t}}function S(e,n,o,f,t){let c=[],i=t.split(`
+`),a=f.split(`
+`);n:for(let r=0;r<=a.length-i.length;r++){c=[];for(let s=0;s0?`
+`:""),h={offset:e+P.length+l,line:n+r+s,column:(r+s===0?o:0)+l},w={offset:h.offset+k.length,line:h.line,column:h.column+k.length};c.push({start:h,end:w})}break}if(c.length===0)throw new Error(`Cannot find ${JSON.stringify(t)} in ${JSON.stringify(f)}`);let p=c[0].start,d=c.at(-1).end;return c.length>1&&c.at(-1).start.offset===d.offset&&(c=c.slice(0,-1)),{lines:c,start:p,end:d}}var g=F(()=>{"use strict"});module.exports=(g(),b(T)).default;module.exports.addTokenPositions=(g(),b(T)).addTokenPositions;
+
+if(__exports != exports)module.exports = exports;return module.exports}));
+//# sourceMappingURL=index.umd.js.map
diff --git a/static/js/lib/node_modules/marked-token-position/lib/index.umd.js.map b/static/js/lib/node_modules/marked-token-position/lib/index.umd.js.map
new file mode 100644
index 0000000..7894776
--- /dev/null
+++ b/static/js/lib/node_modules/marked-token-position/lib/index.umd.js.map
@@ -0,0 +1,7 @@
+{
+ "version": 3,
+ "sources": ["../src/index.ts", ""],
+ "sourcesContent": ["/* node:coverage ignore next */\nimport type { MarkedExtension, Token, Tokens } from 'marked';\n\nexport interface TokenWithPosition extends Tokens.Generic {\n position: Position;\n}\ninterface Position {\n /**\n * Positions for each line of the token. LinePositions will not include the newline character for the line.\n */\n lines: LinePosition[]\n /**\n * Position at the beginning of token\n */\n start: PositionFields;\n /**\n * Position at the end of token\n */\n end: PositionFields;\n}\n\ninterface LinePosition {\n /**\n * Position at the beginning of line\n */\n start: PositionFields;\n /**\n * Position at the end of line. Will not include the newline character.\n */\n end: PositionFields;\n}\n\ninterface PositionFields {\n /**\n * Number of characters from the beginning of the markdown string\n */\n offset: number;\n /**\n * Line number of the token. Starts at line 0.\n */\n line: number;\n /**\n * Column number of the token. Starts at column 0.\n */\n column: number;\n}\n\n/**\n * Add position field to tokens\n */\nexport function addTokenPositions(tokens: Token[]) {\n const markdown = tokens.map(token => token.raw).join('');\n return addPosition(tokens, 0, 0, 0, markdown).tokens;\n}\n\n/**\n * Marked extension to add position field to tokens\n */\nexport default function(options = {}): MarkedExtension {\n return {\n hooks: {\n processAllTokens(tokens) {\n return addTokenPositions(tokens);\n },\n },\n };\n}\n\nfunction addPosition(tokens: Token[], offset: number, line: number, column: number, markdown: string) {\n for (const token of tokens) {\n const genericToken = token as Tokens.Generic;\n const position = getPosition(offset, line, column, markdown, genericToken.raw);\n genericToken.position = position;\n\n if (genericToken.tokens) {\n addPosition(genericToken.tokens, offset, line, column, markdown);\n }\n\n if (genericToken.childTokens) {\n let nextOffset = offset;\n let nextLine = line;\n let nextColumn = column;\n let nextMarkdown = markdown;\n for (const childToken of genericToken.childTokens) {\n const nextPosition = addPosition(genericToken[childToken], nextOffset, nextLine, nextColumn, nextMarkdown);\n nextOffset = nextPosition.offset;\n nextLine = nextPosition.line;\n nextColumn = nextPosition.column;\n nextMarkdown = nextPosition.markdown;\n }\n }\n\n if (genericToken.type === 'list') {\n addPosition(genericToken.items, offset, line, column, markdown);\n }\n\n if (genericToken.type === 'table') {\n let nextOffset = offset;\n let nextLine = line;\n let nextColumn = column;\n let nextMarkdown = markdown;\n for (const headerCell of genericToken.header) {\n const nextPosition = addPosition(headerCell.tokens, nextOffset, nextLine, nextColumn, nextMarkdown);\n nextOffset = nextPosition.offset;\n nextLine = nextPosition.line;\n nextColumn = nextPosition.column;\n nextMarkdown = nextPosition.markdown;\n }\n for (const row of genericToken.rows) {\n for (const rowCell of row) {\n const nextPosition = addPosition(rowCell.tokens, nextOffset, nextLine, nextColumn, nextMarkdown);\n nextOffset = nextPosition.offset;\n nextLine = nextPosition.line;\n nextColumn = nextPosition.column;\n nextMarkdown = nextPosition.markdown;\n }\n }\n }\n\n const deltaOffset = position.end.offset - offset;\n offset = position.end.offset;\n line = position.end.line;\n column = position.end.column;\n markdown = markdown.slice(deltaOffset);\n }\n\n return {\n tokens: tokens as TokenWithPosition[],\n offset,\n line,\n column,\n markdown,\n };\n}\n\nfunction getPosition(offset: number, line: number, column: number, markdown: string, raw: string): Position {\n let lines: LinePosition[] = [];\n const rawLines = raw.split('\\n');\n const markdownLines = markdown.split('\\n');\n\n // eslint-disable-next-line no-labels\n md: for (let i = 0; i <= markdownLines.length - rawLines.length; i++) {\n lines = [];\n for (let j = 0; j < rawLines.length; j++) {\n const markdownLine = markdownLines[i + j];\n const rawLine = rawLines[j];\n const lineStartOffset = markdownLine.indexOf(rawLine);\n\n if (lineStartOffset === -1) {\n // eslint-disable-next-line no-labels\n continue md;\n }\n\n const beforeMarkdownLines = markdownLines.slice(0, i + j).join('\\n') + (i + j > 0 ? '\\n' : '');\n const start = {\n offset: offset + beforeMarkdownLines.length + lineStartOffset,\n line: line + i + j,\n column: (i + j === 0 ? column : 0) + lineStartOffset,\n };\n const end = {\n offset: start.offset + rawLine.length,\n line: start.line,\n column: start.column + rawLine.length,\n };\n\n lines.push({\n start,\n end,\n });\n }\n break;\n }\n\n /* node:coverage ignore next 4 */\n if (lines.length === 0) {\n // This shouldn't ever happen but if it does it would be nice to have a good error message\n throw new Error(`Cannot find ${JSON.stringify(raw)} in ${JSON.stringify(markdown)}`);\n }\n\n const start = lines[0].start;\n const end = lines.at(-1)!.end;\n\n if (lines.length > 1 && lines.at(-1)!.start.offset === end.offset) {\n lines = lines.slice(0, -1);\n }\n\n return {\n lines,\n start,\n end,\n };\n}\n", "\nmodule.exports = require(\"./src/index.ts\").default;\nmodule.exports.addTokenPositions = require(\"./src/index.ts\").addTokenPositions;\n"],
+ "mappings": "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",
+ "names": ["src_exports", "__export", "addTokenPositions", "src_default", "tokens", "markdown", "token", "addPosition", "options", "offset", "line", "column", "genericToken", "position", "getPosition", "nextOffset", "nextLine", "nextColumn", "nextMarkdown", "childToken", "nextPosition", "headerCell", "row", "rowCell", "deltaOffset", "raw", "lines", "rawLines", "markdownLines", "md", "i", "j", "markdownLine", "rawLine", "lineStartOffset", "beforeMarkdownLines", "start", "end", "init_src", "__esmMin"]
+}
diff --git a/static/js/lib/node_modules/marked-token-position/package.json b/static/js/lib/node_modules/marked-token-position/package.json
new file mode 100644
index 0000000..4f359e9
--- /dev/null
+++ b/static/js/lib/node_modules/marked-token-position/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "marked-token-position",
+ "version": "2.0.2",
+ "description": "marked extension template",
+ "main": "./lib/index.esm.js",
+ "module": "./lib/index.esm.js",
+ "browser": "./lib/index.umd.js",
+ "type": "module",
+ "keywords": [
+ "marked",
+ "extension"
+ ],
+ "files": [
+ "lib/",
+ "src/"
+ ],
+ "exports": {
+ ".": {
+ "typescript": "./src/index.ts",
+ "types": "./lib/index.d.ts",
+ "default": "./lib/index.esm.js"
+ }
+ },
+ "scripts": {
+ "build": "npm run build:esbuild && npm run build:types",
+ "build:esbuild": "node esbuild.config.js",
+ "build:types": "tsc && dts-bundle-generator --export-referenced-types --project tsconfig.json -o lib/index.d.ts src/index.ts",
+ "format": "eslint --fix",
+ "lint": "eslint",
+ "test": "npm run build:esbuild && node --experimental-transform-types ./spec/test.config.js",
+ "test:cover": "npm run build:esbuild && node --experimental-transform-types --experimental-test-coverage ./spec/test.config.js -- --cover",
+ "test:only": "npm run build:esbuild && node --experimental-transform-types ./spec/test.config.js -- --only",
+ "test:types": "npm run build:types && tsc --project tsconfig-test-types.json && attw -P --entrypoints . --profile esm-only",
+ "test:update": "npm run build:esbuild && node --experimental-transform-types --test-update-snapshots ./spec/test.config.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/UziTech/marked-token-position.git"
+ },
+ "author": "Tony Brix (https://Tony.Brix.ninja)",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/UziTech/marked-token-position/issues"
+ },
+ "homepage": "https://github.com/UziTech/marked-token-position#readme",
+ "peerDependencies": {
+ "marked": ">=16.2.0 <19"
+ },
+ "devDependencies": {
+ "@arethetypeswrong/cli": "^0.18.2",
+ "@markedjs/eslint-config": "^1.0.14",
+ "@semantic-release/changelog": "^6.0.3",
+ "@semantic-release/commit-analyzer": "^13.0.1",
+ "@semantic-release/git": "^10.0.1",
+ "@semantic-release/github": "^12.0.6",
+ "@semantic-release/npm": "^13.1.5",
+ "@semantic-release/release-notes-generator": "^14.1.0",
+ "dts-bundle-generator": "^9.5.1",
+ "esbuild": "^0.28.0",
+ "esbuild-plugin-umd-wrapper": "^3.0.0",
+ "eslint": "^10.2.0",
+ "marked": "^18.0.0",
+ "semantic-release": "^25.0.3",
+ "typescript": "^6.0.2"
+ }
+}
diff --git a/static/js/lib/node_modules/marked-token-position/src/index.ts b/static/js/lib/node_modules/marked-token-position/src/index.ts
new file mode 100644
index 0000000..d624389
--- /dev/null
+++ b/static/js/lib/node_modules/marked-token-position/src/index.ts
@@ -0,0 +1,192 @@
+/* node:coverage ignore next */
+import type { MarkedExtension, Token, Tokens } from 'marked';
+
+export interface TokenWithPosition extends Tokens.Generic {
+ position: Position;
+}
+interface Position {
+ /**
+ * Positions for each line of the token. LinePositions will not include the newline character for the line.
+ */
+ lines: LinePosition[]
+ /**
+ * Position at the beginning of token
+ */
+ start: PositionFields;
+ /**
+ * Position at the end of token
+ */
+ end: PositionFields;
+}
+
+interface LinePosition {
+ /**
+ * Position at the beginning of line
+ */
+ start: PositionFields;
+ /**
+ * Position at the end of line. Will not include the newline character.
+ */
+ end: PositionFields;
+}
+
+interface PositionFields {
+ /**
+ * Number of characters from the beginning of the markdown string
+ */
+ offset: number;
+ /**
+ * Line number of the token. Starts at line 0.
+ */
+ line: number;
+ /**
+ * Column number of the token. Starts at column 0.
+ */
+ column: number;
+}
+
+/**
+ * Add position field to tokens
+ */
+export function addTokenPositions(tokens: Token[]) {
+ const markdown = tokens.map(token => token.raw).join('');
+ return addPosition(tokens, 0, 0, 0, markdown).tokens;
+}
+
+/**
+ * Marked extension to add position field to tokens
+ */
+export default function(options = {}): MarkedExtension {
+ return {
+ hooks: {
+ processAllTokens(tokens) {
+ return addTokenPositions(tokens);
+ },
+ },
+ };
+}
+
+function addPosition(tokens: Token[], offset: number, line: number, column: number, markdown: string) {
+ for (const token of tokens) {
+ const genericToken = token as Tokens.Generic;
+ const position = getPosition(offset, line, column, markdown, genericToken.raw);
+ genericToken.position = position;
+
+ if (genericToken.tokens) {
+ addPosition(genericToken.tokens, offset, line, column, markdown);
+ }
+
+ if (genericToken.childTokens) {
+ let nextOffset = offset;
+ let nextLine = line;
+ let nextColumn = column;
+ let nextMarkdown = markdown;
+ for (const childToken of genericToken.childTokens) {
+ const nextPosition = addPosition(genericToken[childToken], nextOffset, nextLine, nextColumn, nextMarkdown);
+ nextOffset = nextPosition.offset;
+ nextLine = nextPosition.line;
+ nextColumn = nextPosition.column;
+ nextMarkdown = nextPosition.markdown;
+ }
+ }
+
+ if (genericToken.type === 'list') {
+ addPosition(genericToken.items, offset, line, column, markdown);
+ }
+
+ if (genericToken.type === 'table') {
+ let nextOffset = offset;
+ let nextLine = line;
+ let nextColumn = column;
+ let nextMarkdown = markdown;
+ for (const headerCell of genericToken.header) {
+ const nextPosition = addPosition(headerCell.tokens, nextOffset, nextLine, nextColumn, nextMarkdown);
+ nextOffset = nextPosition.offset;
+ nextLine = nextPosition.line;
+ nextColumn = nextPosition.column;
+ nextMarkdown = nextPosition.markdown;
+ }
+ for (const row of genericToken.rows) {
+ for (const rowCell of row) {
+ const nextPosition = addPosition(rowCell.tokens, nextOffset, nextLine, nextColumn, nextMarkdown);
+ nextOffset = nextPosition.offset;
+ nextLine = nextPosition.line;
+ nextColumn = nextPosition.column;
+ nextMarkdown = nextPosition.markdown;
+ }
+ }
+ }
+
+ const deltaOffset = position.end.offset - offset;
+ offset = position.end.offset;
+ line = position.end.line;
+ column = position.end.column;
+ markdown = markdown.slice(deltaOffset);
+ }
+
+ return {
+ tokens: tokens as TokenWithPosition[],
+ offset,
+ line,
+ column,
+ markdown,
+ };
+}
+
+function getPosition(offset: number, line: number, column: number, markdown: string, raw: string): Position {
+ let lines: LinePosition[] = [];
+ const rawLines = raw.split('\n');
+ const markdownLines = markdown.split('\n');
+
+ // eslint-disable-next-line no-labels
+ md: for (let i = 0; i <= markdownLines.length - rawLines.length; i++) {
+ lines = [];
+ for (let j = 0; j < rawLines.length; j++) {
+ const markdownLine = markdownLines[i + j];
+ const rawLine = rawLines[j];
+ const lineStartOffset = markdownLine.indexOf(rawLine);
+
+ if (lineStartOffset === -1) {
+ // eslint-disable-next-line no-labels
+ continue md;
+ }
+
+ const beforeMarkdownLines = markdownLines.slice(0, i + j).join('\n') + (i + j > 0 ? '\n' : '');
+ const start = {
+ offset: offset + beforeMarkdownLines.length + lineStartOffset,
+ line: line + i + j,
+ column: (i + j === 0 ? column : 0) + lineStartOffset,
+ };
+ const end = {
+ offset: start.offset + rawLine.length,
+ line: start.line,
+ column: start.column + rawLine.length,
+ };
+
+ lines.push({
+ start,
+ end,
+ });
+ }
+ break;
+ }
+
+ /* node:coverage ignore next 4 */
+ if (lines.length === 0) {
+ // This shouldn't ever happen but if it does it would be nice to have a good error message
+ throw new Error(`Cannot find ${JSON.stringify(raw)} in ${JSON.stringify(markdown)}`);
+ }
+
+ const start = lines[0].start;
+ const end = lines.at(-1)!.end;
+
+ if (lines.length > 1 && lines.at(-1)!.start.offset === end.offset) {
+ lines = lines.slice(0, -1);
+ }
+
+ return {
+ lines,
+ start,
+ end,
+ };
+}
diff --git a/static/js/lib/node_modules/marked/LICENSE.md b/static/js/lib/node_modules/marked/LICENSE
similarity index 100%
rename from static/js/lib/node_modules/marked/LICENSE.md
rename to static/js/lib/node_modules/marked/LICENSE
diff --git a/static/js/lib/node_modules/marked/README.md b/static/js/lib/node_modules/marked/README.md
index d4ab251..60f0b28 100644
--- a/static/js/lib/node_modules/marked/README.md
+++ b/static/js/lib/node_modules/marked/README.md
@@ -5,7 +5,6 @@
# Marked
[](https://www.npmjs.com/package/marked)
-[](https://cdn.jsdelivr.net/npm/marked/marked.min.js)
[](https://packagephobia.now.sh/result?p=marked)
[](https://www.npmjs.com/package/marked)
[](https://github.com/markedjs/marked/actions)
@@ -18,7 +17,7 @@
## Demo
-Checkout the [demo page](https://marked.js.org/demo/) to see marked in action ⛹️
+Check out the [demo page](https://marked.js.org/demo/) to see Marked in action ⛹️
## Docs
@@ -33,7 +32,7 @@ Also read about:
**Node.js:** Only [current and LTS](https://nodejs.org/en/about/releases/) Node.js versions are supported. End of life Node.js versions may become incompatible with Marked at any point in time.
-**Browser:** Not IE11 :)
+**Browser:** [Baseline Widely Available](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility)
## Installation
@@ -84,7 +83,7 @@ $ marked --help
-
+
-
-
diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl
index f633692..77b74a6 100644
--- a/views/pages/notes2.gotmpl
+++ b/views/pages/notes2.gotmpl
@@ -1,35 +1,33 @@
{{ define "page" }}
-
+
+
{{ end }}
diff --git a/views/pages/offline.gotmpl b/views/pages/offline.gotmpl
new file mode 100644
index 0000000..0c283f9
--- /dev/null
+++ b/views/pages/offline.gotmpl
@@ -0,0 +1,4 @@
+{{ define "page" }}
+Site is offline.
+||ERROR||
+{{ end }}