1
Fork 0
mirror of https://github.com/glassflame/glassflame.github.io.git synced 2024-11-22 08:04:27 +00:00

Continue refactoring #2

This commit is contained in:
Steffo 2023-10-26 04:53:08 +02:00
parent 8b45e78f4c
commit 610c849264
Signed by: steffo
GPG key ID: 2A24051445686895
26 changed files with 663 additions and 288 deletions

View file

@ -1,5 +1,5 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<state> <state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> <option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state> </state>
</component> </component>

View file

@ -5,6 +5,7 @@
<inspection_tool class="CommaExpressionJS" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="CommaExpressionJS" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="CssUnusedSymbol" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="CssUnusedSymbol" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> <inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ES6PreferShortImport" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues"> <option name="myValues">
<value> <value>

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="JavaScriptLibraryMappings"> <component name="JavaScriptLibraryMappings">
<file url="file://$PROJECT_DIR$" libraries="{marked}" />
</component> </component>
</project> </project>

View file

@ -4,6 +4,9 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>WIP: Obsiview</title> <title>WIP: Obsiview</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Interaction scripts -->
<script type="module" src="src/index.mjs"></script>
<!-- Global style -->
<style> <style>
@import "style/light.css"; @import "style/light.css";
@import "style/dark.css"; @import "style/dark.css";
@ -35,103 +38,14 @@
color: var(--color-foreground); color: var(--color-foreground);
} }
</style> </style>
<script type="module" src="src/index.mjs"></script> <!-- Templates -->
<template id="template-display"> <template id="template-vault">
<slot name="display-container">{Displayed content}</slot>
</template>
<template id="template-node-group">
<style> <style>
.node-group {
outline: var(--node-group-border-width) solid var(--color-node);
background-color: color-mix(in srgb, var(--color-node) 20%, var(--color-background));
border-radius: 0 8px 8px 8px;
padding: 12px;
}
.node-group-label {
position: relative;
bottom: 14px;
left: -12px;
transform: translateY(-100%);
display: inline-block;
outline: var(--node-group-border-width) solid var(--color-node);
background-color: color-mix(in srgb, var(--color-node) 20%, var(--color-background));
border-radius: 8px 8px 0 0;
padding: 12px;
}
.node-group-label h1 {
margin: 0;
}
</style> </style>
<aside class="canvas-item node node-group"> <div class="vault">
<h1><slot name="node-group-label">{Group label}</slot></h1> <slot name="vault-child"></slot>
</aside> </div>
</template>
<template id="template-node-file">
<style>
.node-file {
position: absolute;
box-sizing: border-box;
--color-node: var(--color-gray);
outline: var(--node-file-border-width) solid var(--color-node);
background-color: color-mix(in srgb, var(--color-node) 10%, var(--color-background));
border-radius: 8px;
padding: 12px;
overflow-x: clip;
overflow-y: scroll;
}
.node-file-title {
font-size: 2em;
margin-block-start: .67em;
margin-block-end: .67em;
}
</style>
<article class="node-file">
<h1 class="node-file-title">
<slot name="node-title">{Node title}</slot>
</h1>
<slot name="node-contents">{Node contents}</slot>
</article>
</template>
<template id="template-node-text">
<style>
.node-text {
position: absolute;
box-sizing: border-box;
--color-node: var(--color-gray);
outline: var(--node-file-border-width) solid var(--color-node);
background-color: color-mix(in srgb, var(--color-node) 10%, var(--color-background));
border-radius: 8px;
padding: 12px;
overflow-x: clip;
overflow-y: scroll;
}
</style>
<article class="node-text">
<slot name="node-contents">{Node contents}</slot>
</article>
</template>
<template id="template-edge">
<slot name="edge-svg">{Edge SVG}</slot>
</template>
<template id="template-markdown">
<slot name="markdown-contents">{Markdown text}</slot>
</template>
<template id="template-frontmatter">
<style>
.frontmatter {
opacity: 50%;
}
</style>
<pre class="frontmatter"><slot name="frontmatter-contents">{Markdown text}</slot></pre>
</template> </template>
<template id="template-canvas"> <template id="template-canvas">
<style> <style>
@ -162,15 +76,115 @@
<slot name="canvas-nodes">{Canvas nodes}</slot> <slot name="canvas-nodes">{Canvas nodes}</slot>
</div> </div>
</template> </template>
<template id="template-wikilink"> <template id="template-display">
<slot name="display-container">{Displayed content}</slot>
</template>
<template id="template-node-group">
<style> <style>
.wikilink { .node {
color: var(--color-accent); outline: var(--node-group-border-width) solid var(--color-node);
text-decoration: underline 1px solid currentColor; background-color: color-mix(in srgb, var(--color-node) 20%, var(--color-background));
cursor: pointer; border-radius: 0 8px 8px 8px;
padding: 12px;
}
.node-group-label {
position: relative;
bottom: 14px;
left: -12px;
transform: translateY(-100%);
display: inline-block;
outline: var(--node-group-border-width) solid var(--color-node);
background-color: color-mix(in srgb, var(--color-node) 20%, var(--color-background));
border-radius: 8px 8px 0 0;
padding: 12px;
}
.node-group-label-title {
margin: 0;
} }
</style> </style>
<a class="wikilink"><slot name="wikilink-text">{Wikilink text}</slot></a> <div class="canvas-item node node-group">
<aside class="node-group-label">
<h1 class="node-group-label-title"><slot name="node-group-label">{Group label}</slot></h1>
</aside>
</div>
</template>
<template id="template-node-file">
<style>
.node {
outline: var(--node-file-border-width) solid var(--color-node);
background-color: color-mix(in srgb, var(--color-node) 10%, var(--color-background));
border-radius: 8px;
padding: 12px;
}
.node-file {
overflow-x: clip;
overflow-y: scroll;
}
.node-file-label {
font-size: 2em;
margin-block-start: .67em;
margin-block-end: .67em;
}
</style>
<article class="canvas-item node node-file">
<h1 class="node-file-label">
<slot name="node-file-label">{Node title}</slot>
</h1>
<slot name="node-file-contents">{Node contents}</slot>
</article>
</template>
<template id="template-node-text">
<style>
.node {
outline: var(--node-file-border-width) solid var(--color-node);
background-color: color-mix(in srgb, var(--color-node) 10%, var(--color-background));
border-radius: 8px;
padding: 12px;
}
.node-text {
overflow-x: clip;
overflow-y: scroll;
}
</style>
<article class="canvas-item node node-text">
<slot name="node-text-contents">{Node contents}</slot>
</article>
</template>
<template id="template-edge">
<style>
.edge svg line {
stroke: var(--color-node);
stroke-width: var(--edge-width);
}
</style>
<div class="edge">
<slot name="edge-svg">{Edge SVG}</slot>
</div>
</template>
<template id="template-markdown">
<style>
.markdown
</style>
<div class="markdown">
<slot name="markdown-document">{Markdown text}</slot>
</div>
</template>
<template id="template-frontmatter">
<style>
.frontmatter {
opacity: 50%;
}
</style>
<div class="frontmatter">
<slot name="frontmatter-contents">{Markdown text}</slot>
</div>
</template> </template>
<template id="template-hashtag"> <template id="template-hashtag">
<style> <style>
@ -179,7 +193,17 @@
color: var(--color-accent); color: var(--color-accent);
} }
</style> </style>
<span class="hashtag"><slot name="hashtag-text">{#Hashtag}</slot></span> <span class="hashtag"><slot name="hashtag-tag">{#Hashtag}</slot></span>
</template>
<template id="template-wikilink">
<style>
.wikilink {
color: var(--color-accent);
text-decoration: underline 1px solid currentColor;
cursor: pointer;
}
</style>
<a class="wikilink"><slot name="wikilink-anchor">{Wikilink text}</slot></a>
</template> </template>
</head> </head>
<body> <body>

View file

@ -41,12 +41,10 @@ export class CustomElement extends HTMLElement {
* Callback automatically called when this element is added to the DOM. * Callback automatically called when this element is added to the DOM.
*/ */
connectedCallback() { connectedCallback() {
// The template to duplicate.
const template = this.constructor.getTemplate()
// The shadow root, the inner contents of the element.. // The shadow root, the inner contents of the element..
const shadow = this.attachShadow({ mode: "open" }) const shadow = this.attachShadow({ mode: "open" })
// The element contained inside the shadow root.. // The element contained inside the shadow root..
this.#instance = template.content.cloneNode(true) this.#instance = this.constructor.template.content.cloneNode(true)
// Call the custom callback. // Call the custom callback.
this.onConnect() this.onConnect()
// Add the instance to the DOM. // Add the instance to the DOM.

View file

@ -14,43 +14,43 @@ export class CanvasElement extends CustomElement {
* The contents of the Canvas, as they were the last time they were updated. * The contents of the Canvas, as they were the last time they were updated.
* @type {string} * @type {string}
*/ */
#contents #document
/** /**
* The contents of the Canvas, as they were the last time they were updated. * The contents of the Canvas, as they were the last time they were updated.
* @returns {string} The raw contents. * @returns {string} The raw contents.
*/ */
get contents() { get document() {
return this.#contents return this.#document
} }
/** /**
* The parsed contents of the Canvas, as they were the last time they were updated. * The parsed contents of the Canvas, as they were the last time they were updated.
* @type {Object} * @type {Object}
*/ */
#parsedContents #parsedDocument
/** /**
* The parsed contents of the Canvas, as they were the last time they were updated. * The parsed contents of the Canvas, as they were the last time they were updated.
* @returns {Object} The parsed contents. * @returns {Object} The parsed contents.
*/ */
get parsedContents() { get parsedDocument() {
return this.#parsedContents return this.#parsedDocument
} }
/** /**
* Update the values of {@link contents} and {@link parsedContents} from the `contents` attribute of the element. * Update the values of {@link document} and {@link parsedDocument} from the `document` attribute of the element.
* @throws SyntaxError If `contents` is not valid JSON. * @throws SyntaxError If `contents` is not valid JSON.
*/ */
recalculateContents() { recalculateContents() {
this.#contents = this.getAttribute("contents") this.#document = this.getAttribute("contents")
this.#parsedContents = JSON.parse(this.#contents) this.#parsedDocument = JSON.parse(this.#document)
} }
/** /**
* The minimum X node found in the items of this Canvas. * The minimum X node found in the items of this Canvas.
* Used to compute this element's rect. * Used to compute this element's rect.
* Can be computed from {@link contents} with {@link recalculateMinMax}. * Can be computed from {@link document} with {@link recalculateMinMax}.
* @type {{x: number, width: number}} * @type {{x: number, width: number}}
*/ */
minX minX
@ -58,7 +58,7 @@ export class CanvasElement extends CustomElement {
/** /**
* The minimum Y node found in the items of this Canvas. * The minimum Y node found in the items of this Canvas.
* Used to compute this element's rect. * Used to compute this element's rect.
* Can be computed from {@link contents} with {@link recalculateMinMax}. * Can be computed from {@link document} with {@link recalculateMinMax}.
* @type {{y: number, height: number}} * @type {{y: number, height: number}}
*/ */
minY minY
@ -66,7 +66,7 @@ export class CanvasElement extends CustomElement {
/** /**
* The maximum X node found in the items of this Canvas. * The maximum X node found in the items of this Canvas.
* Used to compute this element's rect. * Used to compute this element's rect.
* Can be computed from {@link contents} with {@link recalculateMinMax}. * Can be computed from {@link document} with {@link recalculateMinMax}.
* @type {{x: number, width: number}} * @type {{x: number, width: number}}
*/ */
maxX maxX
@ -74,13 +74,13 @@ export class CanvasElement extends CustomElement {
/** /**
* The maximum Y node found in the items of this Canvas. * The maximum Y node found in the items of this Canvas.
* Used to compute this element's rect. * Used to compute this element's rect.
* Can be computed from {@link contents} with {@link recalculateMinMax}. * Can be computed from {@link document} with {@link recalculateMinMax}.
* @type {{y: number, height: number}} * @type {{y: number, height: number}}
*/ */
maxY maxY
/** /**
* Compute {@link minX}, {@link minY}, {@link maxX}, {@link maxY} from {@link contents}. * Compute {@link minX}, {@link minY}, {@link maxX}, {@link maxY} from {@link document}.
* @returns {void} * @returns {void}
*/ */
recalculateMinMax() { recalculateMinMax() {
@ -90,7 +90,7 @@ export class CanvasElement extends CustomElement {
this.maxX = { x: -Infinity, width: 0 } this.maxX = { x: -Infinity, width: 0 }
this.maxY = { y: -Infinity, height: 0 } this.maxY = { y: -Infinity, height: 0 }
// Iterate over nodes. // Iterate over nodes.
for(const node of this.parsedContents["nodes"]) { for(const node of this.parsedDocument["nodes"]) {
// Convert node values from strings to numbers. // Convert node values from strings to numbers.
let {x, y, width, height} = node let {x, y, width, height} = node
x, y, width, height = Number(x), Number(y), Number(width), Number(height) x, y, width, height = Number(x), Number(y), Number(width), Number(height)
@ -142,7 +142,7 @@ export class CanvasElement extends CustomElement {
static NODE_ELEMENT_NAME_PREFIX = "x-node-" static NODE_ELEMENT_NAME_PREFIX = "x-node-"
/** /**
* Destroy and recreate the {@link nodesContainer} with the current {@link parsedContents}, {@link minX}, {@link minY}, {@link maxX}, {@link maxY}. * Destroy and recreate the {@link nodesContainer} with the current {@link parsedDocument}, {@link minX}, {@link minY}, {@link maxX}, {@link maxY}.
* @returns {void} * @returns {void}
*/ */
recreateNodes() { recreateNodes() {
@ -154,7 +154,7 @@ export class CanvasElement extends CustomElement {
this.nodesContainer = document.createElement("div") this.nodesContainer = document.createElement("div")
this.nodesContainer.slot = this.constructor.NODES_SLOT_NAME this.nodesContainer.slot = this.constructor.NODES_SLOT_NAME
for(const node of this.parsedContents["nodes"]) { for(const node of this.parsedDocument["nodes"]) {
let {id, type, color, x, y, width, height} = node let {id, type, color, x, y, width, height} = node
x, y, width, height = Number(x), Number(y), Number(width), Number(height) x, y, width, height = Number(x), Number(y), Number(width), Number(height)
@ -228,7 +228,7 @@ export class CanvasElement extends CustomElement {
static EDGE_ELEMENT_NAME = "x-edge" static EDGE_ELEMENT_NAME = "x-edge"
/** /**
* Destroy and recreate the {@link edgesContainer} with the current {@link parsedContents}, {@link minX}, {@link minY}, {@link maxX}, {@link maxY}. * Destroy and recreate the {@link edgesContainer} with the current {@link parsedDocument}, {@link minX}, {@link minY}, {@link maxX}, {@link maxY}.
* @returns {void} * @returns {void}
*/ */
recreateEdges() { recreateEdges() {
@ -240,7 +240,7 @@ export class CanvasElement extends CustomElement {
this.edgesContainer = document.createElement("div") this.edgesContainer = document.createElement("div")
this.edgesContainer.slot = this.constructor.EDGES_SLOT_NAME this.edgesContainer.slot = this.constructor.EDGES_SLOT_NAME
for(const edge of this.parsedContents["edges"]) { for(const edge of this.parsedDocument["edges"]) {
let {id, fromNode, fromSide, toNode, toSide, color, toEnd: arrows} = edge let {id, fromNode, fromSide, toNode, toSide, color, toEnd: arrows} = edge
const element = document.createElement(this.constructor.EDGE_ELEMENT_NAME) const element = document.createElement(this.constructor.EDGE_ELEMENT_NAME)
@ -263,6 +263,7 @@ export class CanvasElement extends CustomElement {
} }
onConnect() { onConnect() {
super.onConnect()
this.recalculateContents() this.recalculateContents()
this.recalculateMinMax() this.recalculateMinMax()
this.recreateNodes() this.recreateNodes()

View file

@ -1,4 +1,4 @@
import { CustomElement } from "src/elements/base.mjs"; import { CustomElement } from "../base.mjs";
/** /**

View file

@ -1,13 +1,13 @@
import { CanvasElement } from "src/elements/canvas/canvas.mjs"; import { CanvasElement } from "../canvas.mjs";
import { CanvasItemElement } from "src/elements/canvas/canvasitem.mjs"; import { CanvasItemElement } from "../canvasitem.mjs";
import { findFirstAncestor } from "src/utils/trasversal.mjs"; import { findFirstAncestor } from "../../../utils/trasversal.mjs";
/** /**
* An edge of a {@link CanvasElement}. * An edge of a {@link CanvasElement}.
*/ */
export class EdgeElement extends CanvasItemElement { export class EdgeElement extends CanvasItemElement {
static getTemplate() { static get template() {
return document.getElementById("template-edge") return document.getElementById("template-edge")
} }
@ -22,7 +22,7 @@ export class EdgeElement extends CanvasItemElement {
* Recalculate the value of {@link canvas}. * Recalculate the value of {@link canvas}.
*/ */
recalculateCanvas() { recalculateCanvas() {
findFirstAncestor(this, CanvasElement) this.canvas = findFirstAncestor(this, CanvasElement)
} }
/** /**
@ -94,15 +94,37 @@ export class EdgeElement extends CanvasItemElement {
*/ */
lineElement lineElement
// TODO: Last time, you were here! static SVG_ELEMENT_SLOT = "edge-svg"
/** /**
* Recreate {@link svgElement} and {@link lineElement} with the current values of the element. * Recreate {@link svgElement} and {@link lineElement} with the current values of the element.
* @returns {void} * @returns {void}
*/ */
recreateSvgElement() { recreateSvgElement() {
if(this.svgElement) {
this.svgElement.remove()
this.svgElement = null
this.lineElement = null
}
const [x1, y1] = this.fromNode.edgeHandle(this.nodeFromSide) const [x1, y1] = this.fromNode.edgeHandle(this.nodeFromSide)
const [x2, y2] = this.toNode.edgeHandle(this.nodeToSide) const [x2, y2] = this.toNode.edgeHandle(this.nodeToSide)
this.lineElement = document.createElementNS("http://www.w3.org/2000/svg", "line")
this.lineElement.setAttribute("x1", x1.toString())
this.lineElement.setAttribute("y1", y1.toString())
this.lineElement.setAttribute("x2", x2.toString())
this.lineElement.setAttribute("y2", y2.toString())
this.svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg")
this.svgElement.slot = this.constructor.SVG_ELEMENT_SLOT
this.svgElement.style.setProperty("position", "absolute")
this.svgElement.style.setProperty("left", "0")
this.svgElement.style.setProperty("top", "0")
this.svgElement.style.setProperty("overflow", "visible")
this.svgElement.appendChild(this.lineElement)
this.appendChild(this.svgElement)
} }
onConnect() { onConnect() {
@ -110,31 +132,5 @@ export class EdgeElement extends CanvasItemElement {
this.recalculateCanvas() this.recalculateCanvas()
this.recalculateFromTo() this.recalculateFromTo()
this.recreateSvgElement() this.recreateSvgElement()
const fromNode = canvas.nodeElements[this.getAttribute("node-from")]
const fromSide = this.getAttribute("node-from-side")
const [x1, y1] = fromNode.getCenterCoordinatesOfSide(fromSide)
const toNode = canvas.nodeElements[this.getAttribute("node-to")]
const toSide = this.getAttribute("node-to-side")
const [x2, y2] = toNode.getCenterCoordinatesOfSide(toSide)
this.svgSlotted = document.createElementNS("http://www.w3.org/2000/svg", "svg")
this.svgSlotted.slot = "edge-svg"
this.svgSlotted.style.setProperty("position", "absolute")
this.svgSlotted.style.setProperty("left", "0")
this.svgSlotted.style.setProperty("top", "0")
this.svgSlotted.style.setProperty("overflow", "visible")
this.lineElement = document.createElementNS("http://www.w3.org/2000/svg", "line")
this.lineElement.setAttribute("x1", x1)
this.lineElement.setAttribute("y1", y1)
this.lineElement.setAttribute("x2", x2)
this.lineElement.setAttribute("y2", y2)
this.lineElement.style.setProperty("stroke", this.constructor.colorToCSS(this.getAttribute("color")))
this.lineElement.style.setProperty("stroke-width", "var(--edge-width)")
this.svgSlotted.appendChild(this.lineElement)
this.appendChild(this.svgSlotted)
} }
} }

View file

@ -0,0 +1 @@
export {EdgeElement} from "./base.mjs"

View file

@ -0,0 +1,3 @@
export {CanvasElement} from "./canvas.mjs"
export {EdgeElement} from "./edge/index.mjs"
export {NodeFileElement, NodeGroupElement, NodeTextElement} from "./node/index.mjs"

View file

@ -1,4 +1,4 @@
import { CanvasItemElement } from "src/elements/canvas/canvasitem.mjs"; import { CanvasItemElement } from "../canvasitem.mjs";
/** /**

View file

@ -1,11 +1,11 @@
import { NodeElement } from "src/elements/canvas/node/base.mjs"; import { NodeElement } from "./base.mjs";
import { DisplayElement } from "src/elements/display.mjs"; import { DisplayElement } from "../../display.mjs";
import { fileDetails } from "src/utils/file.mjs"; import { fileDetails } from "../../../utils/file.mjs";
import { findFirstAncestor } from "src/utils/trasversal.mjs"; import { findFirstAncestor } from "../../../utils/trasversal.mjs";
export class NodeFileElement extends NodeElement { export class NodeFileElement extends NodeElement {
static getTemplate() { static get template() {
return document.getElementById("template-node-file") return document.getElementById("template-node-file")
} }

View file

@ -1,4 +1,4 @@
import { NodeElement } from "src/elements/canvas/node/base.mjs"; import { NodeElement } from "./base.mjs";
/** /**
@ -6,6 +6,10 @@ import { NodeElement } from "src/elements/canvas/node/base.mjs";
* Visual only, does not actually contain any other nodes. * Visual only, does not actually contain any other nodes.
*/ */
export class NodeGroupElement extends NodeElement { export class NodeGroupElement extends NodeElement {
static get template() {
return document.getElementById("template-node-group")
}
/** /**
* The label text of the group. * The label text of the group.
* Obtained from the `label` attribute of the element. * Obtained from the `label` attribute of the element.
@ -41,10 +45,6 @@ export class NodeGroupElement extends NodeElement {
this.appendChild(this.labelSlotted) this.appendChild(this.labelSlotted)
} }
static getTemplate() {
return document.getElementById("template-node-group")
}
onConnect() { onConnect() {
super.onConnect() super.onConnect()
this.recreateLabelElement() this.recreateLabelElement()

View file

@ -0,0 +1,3 @@
export {NodeFileElement} from "./file.mjs"
export {NodeGroupElement} from "./group.mjs"
export {NodeTextElement} from "./text.mjs"

View file

@ -1,12 +1,11 @@
import { NodeElement } from "src/elements/canvas/node/base.mjs"; import { NodeElement } from "./base.mjs"
import { DisplayElement } from "src/elements/display.mjs";
/** /**
* A {@link NodeElement} directly rendering a Markdown document. * A {@link NodeElement} directly rendering a Markdown document.
*/ */
export class NodeTextElement extends NodeElement { export class NodeTextElement extends NodeElement {
static getTemplate() { static get template() {
return document.getElementById("template-node-text") return document.getElementById("template-node-text")
} }
@ -14,11 +13,12 @@ export class NodeTextElement extends NodeElement {
* Get the Markdown source of this node from the `document` attribute. * Get the Markdown source of this node from the `document` attribute.
*/ */
get markdownDocument() { get markdownDocument() {
return this.getAttribute("text") return this.getAttribute("document")
} }
/** /**
* The element displaying the contents of the node. * The element displaying the contents of the node.
* Can be recreated with {@link recreateContentsElement}.
* @type {MarkdownElement} * @type {MarkdownElement}
*/ */
contentsElement contentsElement
@ -27,7 +27,7 @@ export class NodeTextElement extends NodeElement {
* The name of the slot where {@link contentsElement} should be placed in. * The name of the slot where {@link contentsElement} should be placed in.
* @type {string} * @type {string}
*/ */
static CONTENTS_ELEMENT_SLOT = "node-file-contents" static CONTENTS_ELEMENT_SLOT = "node-text-contents"
/** /**
* Recreate {@link labelElement} with the current value of {@link fileName}. * Recreate {@link labelElement} with the current value of {@link fileName}.

View file

@ -1,58 +1,129 @@
import { VaultElement } from "./vault.mjs";
import { findFirstAncestor } from "../utils/trasversal.mjs";
import { fileDetails } from "../utils/file.mjs"; import { fileDetails } from "../utils/file.mjs";
import { CanvasElement } from "./canvas/canvas.mjs";
import { MarkdownElement } from "./markdown.mjs";
import { FetchError } from "src/elements/canvas/node/base.mjs";
import { CustomElement } from "./base.mjs"; import { CustomElement } from "./base.mjs";
/**
* Element loading and displaying the contents of a remote file.
*/
export class DisplayElement extends CustomElement { export class DisplayElement extends CustomElement {
static getTemplate() { static get template() {
return document.getElementById("template-display") return document.getElementById("template-display")
} }
containerSlotted /**
loadButton * The vault this element is displaying content from.
* Can be recalculated via {@link recalculateVault}.
* @type {VaultElement}
*/
vault
/**
* Recalculate the value of {@link vault}.
*/
recalculateVault() {
this.vault = findFirstAncestor(this, VaultElement)
}
/**
* Get the path or name of the file this node points to.
* @returns {string} The value in question.
*/
get target() {
return this.getAttribute("target")
}
/**
* An element displaying the loading status of the current element.
* @type {HTMLDivElement|null}
*/
loadingElement = null
/**
* An element displaying the contents of the current element.
* @type {HTMLDivElement|null}
*/
contentsElement = null
/**
* Slot shared by both {@link loadingElement} and {@link contentsElement}.
* @type {string}
*/
static CONTAINER_ELEMENT_SLOT = "display-container"
/**
* Recreate {@link loadingElement}, removing {@link contentsElement} if it exists.
*/
recreateLoadingElement() {
if(this.loadingElement !== null) {
this.loadingElement.remove()
this.loadingElement = null
}
if(this.contentsElement !== null) {
this.contentsElement.remove()
this.contentsElement = null
}
this.loadingElement = document.createElement("div")
this.loadingElement.slot = this.constructor.CONTAINER_ELEMENT_SLOT
this.loadingElement.innerText = "Loading..."
this.appendChild(this.loadingElement)
}
/**
* Recreate {@link contentsElement}, removing {@link loadingElement} if it exists.
*/
recreateContentsElement() {
if(this.loadingElement !== null) {
this.loadingElement.remove()
this.loadingElement = null
}
if(this.contentsElement !== null) {
this.contentsElement.remove()
this.contentsElement = null
}
const {extension} = fileDetails(this.target)
switch(extension) {
case "md":
this.contentsElement = document.createElement("x-markdown")
break
case "canvas":
this.contentsElement = document.createElement("x-canvas")
break
default:
console.warn("Encountered a file with an unknown extension:", extension)
return
}
this.contentsElement.setAttribute("document", this.document)
this.contentsElement.slot = this.constructor.CONTAINER_ELEMENT_SLOT
this.appendChild(this.loadingElement)
}
/**
* The plaintext contents of the {@link target} document.
* @type {string}
*/
document
/**
* Reload the {@link target} {@link document}.
* @returns {Promise<void>}
*/
async reloadDocument() {
const response = await this.vault.fetchCooldown(this.target)
// TODO: Add a check that the request was successful
this.document = await response.text()
}
onConnect() { onConnect() {
this.containerSlotted = document.createElement("div") super.onConnect()
this.containerSlotted.slot = "display-container" this.recalculateVault()
this.loadButton = document.createElement("button") this.recreateLoadingElement()
this.loadButton.innerText = "Load" // noinspection JSIgnoredPromiseFromCall
this.loadButton.addEventListener("click", this.load.bind(this)) this.reloadDocument().then(this.recreateContentsElement)
this.containerSlotted.appendChild(this.loadButton)
this.appendChild(this.containerSlotted)
}
data
async fetchData() {
const vref = this.getAttribute("vref")
const wref = this.getAttribute("wref")
const url = new URL(wref, vref)
const response = await fetch(url, {})
if(!response.ok) throw new FetchError(response, "Fetch response is not ok")
this.data = await response.text()
}
async load() {
this.loadButton.disabled = true
await this.fetchData()
this.containerSlotted.remove()
this.containerSlotted = null
const fileExtension = fileDetails(this.getAttribute("wref")).extension
this.containerSlotted = document.createElement({
"md": customElements.getName(MarkdownElement),
"canvas": customElements.getName(CanvasElement),
}[fileExtension])
this.containerSlotted.slot = "display-container"
this.containerSlotted.setAttribute("contents", this.data)
this.appendChild(this.containerSlotted)
} }
} }

View file

@ -1,5 +1,4 @@
export {NodeFileElement, NodeGroupElement, NodeTextElement} from "src/elements/canvas/node/base.mjs" export {CanvasElement, NodeFileElement, NodeGroupElement, NodeTextElement, EdgeElement} from "./canvas/index.mjs"
export {MarkdownElement, HashtagElement, WikilinkElement, FrontMatterElement} from "./markdown.mjs" export {MarkdownElement, HashtagElement, WikilinkElement, FrontMatterElement} from "./markdown/index.mjs"
export {CanvasElement} from "./canvas/canvas.mjs"
export {DisplayElement} from "./display.mjs" export {DisplayElement} from "./display.mjs"
export {EdgeElement} from "src/elements/canvas/edge/base.mjs" export {VaultElement} from "./vault.mjs"

View file

@ -0,0 +1,73 @@
import { CustomElement } from "../base.mjs";
/**
* Element rendering Obsidian front matter.
*/
export class FrontMatterElement extends CustomElement {
static get template() {
return document.getElementById("template-frontmatter")
}
/**
* The programming language used to define this front matter, obtained from the `lang` attribute.
* @type {string}
*/
get language() {
return this.getAttribute("lang")
}
/**
* The text contained in this front matter, obtained from the `data` attribute.
*/
get data() {
return this.getAttribute("data")
}
/**
* The element marking the front matter as preformatted text.
* Can be recreated with {@link recreatePreCodeElement}.
* @type {HTMLPreElement}
*/
preElement
/**
* The name of the slot where {@link preElement} should be placed in.
* @type {string}
*/
static PRE_ELEMENT_SLOT = "frontmatter-contents"
/**
* The element displaying the code of the front matter.
* Can be recreated with {@link recreatePreCodeElement}.
* @type {HTMLElement}
*/
codeElement
/**
* Recreate {@link preElement} and {@link codeElement} with the current value of {@link language} and {@link data}.
* @returns {void}
*/
recreatePreCodeElement() {
if(this.preElement) {
this.preElement.remove()
this.preElement = null
this.codeElement = null
}
this.codeElement = document.createElement("code")
this.codeElement.setAttribute("lang", this.language)
this.codeElement.innerText = this.data
this.preElement = document.createElement("pre")
this.preElement.slot = this.constructor.PRE_ELEMENT_SLOT
this.preElement.appendChild(this.codeElement)
this.appendChild(this.preElement)
}
onConnect() {
super.onConnect()
this.recreatePreCodeElement()
}
}

View file

@ -0,0 +1,52 @@
import { CustomElement } from "../base.mjs";
/**
* Element rendering an Obsidian Hashtag.
*/
export class HashtagElement extends CustomElement {
static get template() {
return document.getElementById("template-hashtag")
}
/**
* The name of the hashtag, with no leading hash, obtained from the `tag` attribute.
* @returns {string}
*/
get tag() {
return this.getAttribute("tag")
}
/**
* The element displaying the hashtag.
* Can be recreated with {@link recreateTagElement}.
* @type {HTMLSpanElement}
*/
tagElement
/**
* The name of the slot where {@link tagElement} should be placed in.
* @type {string}
*/
static TAG_ELEMENT_SLOT = "hashtag-tag"
/**
* Recreate {@link tagElement} with the current value of {@link tag}.
*/
recreateTagElement() {
if(this.tagElement) {
this.tagElement.remove()
this.tagElement = null
}
this.tagElement = document.createElement("span")
this.tagElement.slot = this.constructor.TAG_ELEMENT_SLOT
this.tagElement.innerText = `#${this.tag}`
this.appendChild(this.tagElement)
}
onConnect() {
super.onConnect()
this.recreateTagElement()
}
}

View file

@ -0,0 +1,4 @@
export {MarkdownElement} from "./renderer.mjs"
export {FrontMatterElement} from "./frontmatter.mjs"
export {HashtagElement} from "./hashtag.mjs"
export {WikilinkElement} from "./wikilink.mjs"

View file

@ -1,12 +1,20 @@
import { Marked } from "https://unpkg.com/marked@9.1.2/lib/marked.esm.js"; import { Marked } from "https://unpkg.com/marked@9.1.2/lib/marked.esm.js";
import { CustomElement } from "./base.mjs"; import { CustomElement } from "../base.mjs";
/** /**
* Element rendering the Markdown contents of an Obsidian page. * Element rendering the Markdown contents of an Obsidian page.
*/ */
export class MarkdownElement extends CustomElement { export class MarkdownElement extends CustomElement {
static marked = new Marked({ static get template() {
return document.getElementById("template-markdown")
}
/**
* {@link Marked} Markdown renderer.
* @type {Marked}
*/
static MARKED = new Marked({
extensions: [ extensions: [
{ {
name: "frontmatter", name: "frontmatter",
@ -26,7 +34,8 @@ export class MarkdownElement extends CustomElement {
} }
}, },
renderer(token) { renderer(token) {
return `<x-frontmatter><code slot="frontmatter-contents" lang="${token.lang}">${token.data}</code></x-frontmatter>`; // TODO: Doesn't this break if token.data contains quotes?
return `<x-frontmatter lang="${token.lang}" data="${token.data}"></x-frontmatter>`;
} }
}, },
{ {
@ -41,13 +50,13 @@ export class MarkdownElement extends CustomElement {
return { return {
type: "wikilink", type: "wikilink",
raw: match[0], raw: match[0],
wref: match[1], target: match[1],
text: match[2], text: match[2],
} }
} }
}, },
renderer(token) { renderer(token) {
return `<x-wikilink wref="${token.wref}"><span slot="wikilink-text">${token.text ?? token.wref}</span></x-wikilink>` return `<x-wikilink target="${token.target}" text="${token.text}"></x-wikilink>`
}, },
}, },
{ {
@ -67,61 +76,50 @@ export class MarkdownElement extends CustomElement {
} }
}, },
renderer(token) { renderer(token) {
return `<x-hashtag><span slot="hashtag-text">#${token.tag}</span></x-hashtag>` return `<x-hashtag tag="${token.tag}"></x-hashtag>`
} }
} }
] ]
}) })
contentsElement /**
* Markdown source of the document to render, obtained from the `document` attribute.
* @returns {string}
*/
get markdownDocument() {
return this.getAttribute("document")
}
static getTemplate() { /**
return document.getElementById("template-markdown") * Element containing the rendered Markdown source.
* Can be recreated with {@link recreateDocumentElement}.
* @type {HTMLDivElement}
*/
documentElement
/**
* The name of the slot where {@link documentElement} should be placed in.
* @type {string}
*/
static DOCUMENT_ELEMENT_SLOT = "markdown-document"
/**
* Recreate {@link documentElement} using the current value of {@link markdownDocument}.
*/
recreateDocumentElement() {
if(this.documentElement) {
this.documentElement.remove()
this.documentElement = null
}
this.documentElement = document.createElement("div")
this.documentElement.slot = this.constructor.DOCUMENT_ELEMENT_SLOT
this.documentElement.innerHTML = this.constructor.MARKED.parse(this.markdownDocument)
this.appendChild(this.documentElement)
} }
onConnect() { onConnect() {
const markdown = this.getAttribute("contents") super.onConnect()
this.recreateDocumentElement()
this.contentsElement = document.createElement("div")
this.contentsElement.setAttribute("slot", "markdown-contents")
this.contentsElement.innerHTML = MarkdownElement.marked.parse(markdown)
this.appendChild(this.contentsElement)
}
}
/**
* Element rendering Obsidian front matter.
*/
export class FrontMatterElement extends CustomElement {
static getTemplate() {
return document.getElementById("template-frontmatter")
}
}
/**
* Element rendering an Obsidian Hashtag.
*/
export class HashtagElement extends CustomElement {
static getTemplate() {
return document.getElementById("template-hashtag")
}
}
/**
* Element rendering an Obsidian Wikilink.
*/
export class WikilinkElement extends CustomElement {
static getTemplate() {
return document.getElementById("template-wikilink")
}
onConnect() {
const instanceElement = this.instance.querySelector(".wikilink")
const destinationURL = new URL(window.location)
destinationURL.hash = this.getAttribute("wref")
instanceElement.href = destinationURL
} }
} }

View file

@ -0,0 +1,62 @@
import { CustomElement } from "../base.mjs";
/**
* Element rendering an Obsidian Wikilink.
*/
export class WikilinkElement extends CustomElement {
static get template() {
return document.getElementById("template-hashtag")
}
/**
* Get the path or name of the file this node points to.
* @returns {string} The value in question.
*/
get target() {
return this.getAttribute("target")
}
/**
* Get the text that should be displayed in this wikilink.
* @returns {string} The text in question.
*/
get text() {
return this.getAttribute("text") ?? this.target
}
/**
* The element displaying the wikilink.
* Can be recreated with {@link recreateTagElement}.
* @type {HTMLAnchorElement}
*/
anchorElement
/**
* The name of the slot where {@link anchorElement} should be placed in.
* @type {string}
*/
static ANCHOR_ELEMENT_SLOT = "wikilink-anchor"
/**
* Recreate {@link anchorElement} with the current value of {@link target} and {@link text}.
* @returns {void}
*/
recreateTagElement() {
if(this.anchorElement) {
this.anchorElement.remove()
this.anchorElement = null
}
this.anchorElement = document.createElement("a")
this.anchorElement.slot = this.constructor.ANCHOR_ELEMENT_SLOT
this.anchorElement.href = "#" // TODO: Add href behaviour to the anchor.
this.anchorElement.innerText = this.text
this.appendChild(this.anchorElement)
}
onConnect() {
super.onConnect()
this.recreateTagElement()
}
}

86
src/elements/vault.mjs Normal file
View file

@ -0,0 +1,86 @@
import { CustomElement } from "./base.mjs";
import { sleep } from "../utils/sleep.mjs";
/**
* Element storing information about a Vault for its children.
* The first direct children must have a `[slot="vault-child"]` attribute.
*/
export class VaultElement extends CustomElement {
static get template() {
return document.getElementById("template-vault")
}
/**
* The base URL where the Vault is available at.
*/
get base() {
return this.getAttribute("base")
}
/**
* {@link fetch} the file at the given path ignoring cooldowns.
* @param path {string} The path where the file is located.
* @returns {Promise<Response>} The resulting HTTP response.
*/
async fetchImmediately(path) {
const url = new URL(path, this.base)
return await fetch(url, {})
}
/**
* Cooldown between two {@link fetchCooldown} requests in milliseconds, as obtained from the `cooldown` parameter.
*/
get cooldownMs() {
return Number(this.getAttribute("cooldown"))
}
/**
* Queue containing the `resolve` functions necessary to make the calls to {@link fetchCooldown} proceed beyond the waiting phase.
* To be called by the preceding {@link fetchCooldown} if possible.
* @type {((v: unknown) => void)[]}
*/
#fetchQueue = []
/**
* @returns {Promise<void>} A Promise that will wait for this caller's turn in the {@link #fetchQueue}.
*/
fetchQueueTurn() {
return new Promise(resolve => {
this.#fetchQueue.push(resolve)
})
}
/**
* A promise that will advance the fetch queue after {@link cooldownMs}.
* @returns {Promise<void>}
*/
async #scheduleNextFetchQueueTurn() {
await sleep(this.cooldownMs)
const resolve = this.#fetchQueue.shift()
resolve()
}
/**
* {@link fetch} the file at the given path, awaiting for cooldowns to expire.
* @param path {string} The path where the file is located.
* @returns {Promise<Response>} The resulting HTTP response.
*/
async fetchCooldown(path) {
// Sit waiting in queue
if(this.#fetchQueue.length > 0) {
await this.fetchQueueTurn()
}
// Perform the request
const result = await this.fetchImmediately(path)
// Start the next item in queue
// noinspection ES6MissingAwait
this.#scheduleNextFetchQueueTurn()
// Return the request's result
return result
}
onConnect() {
super.onConnect()
}
}

View file

@ -1,5 +1,4 @@
import { CanvasElement, HashtagElement, MarkdownElement, NodeFileElement, WikilinkElement, DisplayElement, EdgeElement, NodeGroupElement, NodeTextElement, FrontMatterElement } from "./elements/index.mjs"; import { CanvasElement, HashtagElement, NodeFileElement, WikilinkElement, DisplayElement, EdgeElement, NodeGroupElement, NodeTextElement, FrontMatterElement, MarkdownElement } from "./elements/index.mjs";
import { configFromWindow } from "./config.mjs";
customElements.define("x-node-file", NodeFileElement) customElements.define("x-node-file", NodeFileElement)
customElements.define("x-node-text", NodeTextElement) customElements.define("x-node-text", NodeTextElement)
@ -11,10 +10,3 @@ customElements.define("x-hashtag", HashtagElement)
customElements.define("x-canvas", CanvasElement) customElements.define("x-canvas", CanvasElement)
customElements.define("x-display", DisplayElement) customElements.define("x-display", DisplayElement)
customElements.define("x-edge", EdgeElement) customElements.define("x-edge", EdgeElement)
const config = configFromWindow()
const displayElement = document.createElement("x-display")
displayElement.setAttribute("vref", config.vref)
displayElement.setAttribute("wref", config.wref)
displayElement.setAttribute("root", "")
document.body.appendChild(displayElement)

View file

@ -1,7 +1,12 @@
export function filePath(file) { /**
* Compute a file path, resolving `.` and `..`.
* @param path The file path.
* @returns {string[]} Array of directories from the root of the path to the target location.
*/
export function filePath(path) {
const stack = [] const stack = []
for(const part of file.split("/")) { for(const part of path.split("/")) {
if(part === ".") { if(part === ".") {
// noinspection UnnecessaryContinueJS // noinspection UnnecessaryContinueJS
continue continue

7
src/utils/sleep.mjs Normal file
View file

@ -0,0 +1,7 @@
/**
* Sleep.
* @returns {Promise<void>}
*/
export async function sleep(timeMs) {
return await new Promise(resolve => setTimeout(resolve, timeMs))
}