bidirectional links
This commit is contained in:
parent
22795467af
commit
1b95e9a8df
1
Gemfile
1
Gemfile
@ -15,6 +15,7 @@ gem "jekyll", "~> 4.2.0"
|
|||||||
# If you have any plugins, put them here!
|
# If you have any plugins, put them here!
|
||||||
group :jekyll_plugins do
|
group :jekyll_plugins do
|
||||||
gem "jekyll-feed", "~> 0.12"
|
gem "jekyll-feed", "~> 0.12"
|
||||||
|
gem "jekyll-last-modified-at", "~> 1.3"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
|
# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||||
|
10
Gemfile.lock
10
Gemfile.lock
@ -11,7 +11,7 @@ GEM
|
|||||||
ethon (0.12.0)
|
ethon (0.12.0)
|
||||||
ffi (>= 1.3.0)
|
ffi (>= 1.3.0)
|
||||||
eventmachine (1.2.7)
|
eventmachine (1.2.7)
|
||||||
ffi (1.14.2)
|
ffi (1.15.0)
|
||||||
forwardable-extended (2.6.0)
|
forwardable-extended (2.6.0)
|
||||||
html-proofer (3.18.8)
|
html-proofer (3.18.8)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
@ -41,6 +41,9 @@ GEM
|
|||||||
terminal-table (~> 2.0)
|
terminal-table (~> 2.0)
|
||||||
jekyll-feed (0.15.1)
|
jekyll-feed (0.15.1)
|
||||||
jekyll (>= 3.7, < 5.0)
|
jekyll (>= 3.7, < 5.0)
|
||||||
|
jekyll-last-modified-at (1.3.0)
|
||||||
|
jekyll (>= 3.7, < 5.0)
|
||||||
|
posix-spawn (~> 0.3.9)
|
||||||
jekyll-sass-converter (2.1.0)
|
jekyll-sass-converter (2.1.0)
|
||||||
sassc (> 2.0.1, < 3.0)
|
sassc (> 2.0.1, < 3.0)
|
||||||
jekyll-watch (2.2.1)
|
jekyll-watch (2.2.1)
|
||||||
@ -56,13 +59,12 @@ GEM
|
|||||||
mercenary (0.4.0)
|
mercenary (0.4.0)
|
||||||
nokogiri (1.11.1-x86_64-darwin)
|
nokogiri (1.11.1-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.11.1-x86_64-linux)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogumbo (2.0.4)
|
nokogumbo (2.0.4)
|
||||||
nokogiri (~> 1.8, >= 1.8.4)
|
nokogiri (~> 1.8, >= 1.8.4)
|
||||||
parallel (1.20.1)
|
parallel (1.20.1)
|
||||||
pathutil (0.16.2)
|
pathutil (0.16.2)
|
||||||
forwardable-extended (~> 2.6)
|
forwardable-extended (~> 2.6)
|
||||||
|
posix-spawn (0.3.15)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.6)
|
||||||
racc (1.5.2)
|
racc (1.5.2)
|
||||||
rainbow (3.0.0)
|
rainbow (3.0.0)
|
||||||
@ -84,12 +86,12 @@ GEM
|
|||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
x86_64-darwin-19
|
x86_64-darwin-19
|
||||||
x86_64-linux
|
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
html-proofer (~> 3.18)
|
html-proofer (~> 3.18)
|
||||||
jekyll (~> 4.2.0)
|
jekyll (~> 4.2.0)
|
||||||
jekyll-feed (~> 0.12)
|
jekyll-feed (~> 0.12)
|
||||||
|
jekyll-last-modified-at (~> 1.3)
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
tzinfo (~> 1.2)
|
tzinfo (~> 1.2)
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
|
23
_config.yml
23
_config.yml
@ -31,9 +31,31 @@ excerpt_separator: "<!--more-->"
|
|||||||
# Build settings
|
# Build settings
|
||||||
plugins:
|
plugins:
|
||||||
- jekyll-feed
|
- jekyll-feed
|
||||||
|
- jekyll-last-modified-at
|
||||||
|
|
||||||
sass:
|
sass:
|
||||||
style: compressed
|
style: compressed
|
||||||
|
|
||||||
|
collections:
|
||||||
|
notes:
|
||||||
|
output: true
|
||||||
|
permalink: /:slug
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
- scope:
|
||||||
|
path: "**/*"
|
||||||
|
values:
|
||||||
|
layout: "default"
|
||||||
|
- scope:
|
||||||
|
path: "_notes/**/*.md"
|
||||||
|
values:
|
||||||
|
layout: "note"
|
||||||
|
|
||||||
|
|
||||||
|
permalink: pretty
|
||||||
|
relative_permalinks: false
|
||||||
|
use_html_extension: false
|
||||||
|
|
||||||
# Exclude from processing.
|
# Exclude from processing.
|
||||||
# The following items will not be processed, by default.
|
# The following items will not be processed, by default.
|
||||||
# Any item listed under the `exclude:` key here will be automatically added to
|
# Any item listed under the `exclude:` key here will be automatically added to
|
||||||
@ -43,6 +65,7 @@ sass:
|
|||||||
# their entries' file path in the `include:` list.
|
# their entries' file path in the `include:` list.
|
||||||
#
|
#
|
||||||
exclude:
|
exclude:
|
||||||
|
- _includes/notes_graph.json
|
||||||
- .sass-cache/
|
- .sass-cache/
|
||||||
- .jekyll-cache/
|
- .jekyll-cache/
|
||||||
- gemfiles/
|
- gemfiles/
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% if page.title %}{{ page.title }} 🌱 {% endif %}{{ site.title }}</title>
|
<title>{% if page.title %}{{ page.title }} 🌱 {% endif %}{{ site.title }}</title>
|
||||||
<meta name="description" content="{{ meta_description }}">
|
<meta name="description" content="{{ meta_description | escape }}">
|
||||||
<link rel="stylesheet" href="{{ "/assets/css/style.css" | relative_url }}">
|
<link rel="stylesheet" href="{{ "/assets/css/style.css" | relative_url }}">
|
||||||
{%- feed_meta -%}
|
{%- feed_meta -%}
|
||||||
</head>
|
</head>
|
86
_includes/link-previews.html
Normal file
86
_includes/link-previews.html
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<!-- That file is not particularly elegant. This will need a refactor at some point. -->
|
||||||
|
<div style="opacity: 0; display: none;" id='tooltip-wrapper'>
|
||||||
|
<div id='tooltip-content'>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<iframe style="display: none; height: 0; width: 0;" id='link-preview-iframe' src="">
|
||||||
|
</iframe>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var opacityTimeout;
|
||||||
|
var contentTimeout;
|
||||||
|
var transitionDurationMs = 100;
|
||||||
|
|
||||||
|
var iframe = document.getElementById('link-preview-iframe')
|
||||||
|
var tooltipWrapper = document.getElementById('tooltip-wrapper')
|
||||||
|
var tooltipContent = document.getElementById('tooltip-content')
|
||||||
|
|
||||||
|
function hideTooltip() {
|
||||||
|
opacityTimeout = setTimeout(function() {
|
||||||
|
tooltipWrapper.style.opacity = 0;
|
||||||
|
contentTimeout = setTimeout(function() {
|
||||||
|
tooltipContent.innerHTML = '';
|
||||||
|
tooltipWrapper.style.display = 'none';
|
||||||
|
}, transitionDurationMs + 1);
|
||||||
|
}, transitionDurationMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTooltip(event) {
|
||||||
|
var elem = event.target;
|
||||||
|
var elem_props = elem.getClientRects()[elem.getClientRects().length - 1];
|
||||||
|
var top = window.pageYOffset || document.documentElement.scrollTop
|
||||||
|
|
||||||
|
if (event.target.host === window.location.host) {
|
||||||
|
iframe.src = event.target.href
|
||||||
|
iframe.onload = function() {
|
||||||
|
tooltipContentHtml = ''
|
||||||
|
tooltipContentHtml += '<div style="font-weight: bold;">' + iframe.contentWindow.document.querySelector('h1').innerHTML + '</div>'
|
||||||
|
tooltipContentHtml += iframe.contentWindow.document.querySelector('content').innerHTML
|
||||||
|
|
||||||
|
tooltipContent.innerHTML = tooltipContentHtml
|
||||||
|
|
||||||
|
tooltipWrapper.style.display = 'block';
|
||||||
|
setTimeout(function() {
|
||||||
|
tooltipWrapper.style.opacity = 1;
|
||||||
|
}, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltipWrapper.style.left = elem_props.left - (tooltipWrapper.offsetWidth / 2) + (elem_props.width / 2) + "px";
|
||||||
|
if ((window.innerHeight - elem_props.top) < (tooltipWrapper.offsetHeight)) {
|
||||||
|
tooltipWrapper.style.top = elem_props.top + top - tooltipWrapper.offsetHeight - 10 + "px";
|
||||||
|
} else if ((window.innerHeight - elem_props.top) > (tooltipWrapper.offsetHeight)) {
|
||||||
|
tooltipWrapper.style.top = elem_props.top + top + 35 + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((elem_props.left + (elem_props.width / 2)) < (tooltipWrapper.offsetWidth / 2)) {
|
||||||
|
tooltipWrapper.style.left = "10px";
|
||||||
|
} else if ((document.body.clientWidth - elem_props.left - (elem_props.width / 2)) < (tooltipWrapper.offsetWidth / 2)) {
|
||||||
|
tooltipWrapper.style.left = document.body.clientWidth - tooltipWrapper.offsetWidth - 20 + "px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupListeners(linkElement) {
|
||||||
|
linkElement.addEventListener('mouseleave', function(_event) {
|
||||||
|
hideTooltip();
|
||||||
|
});
|
||||||
|
|
||||||
|
tooltipWrapper.addEventListener('mouseleave', function(_event) {
|
||||||
|
hideTooltip();
|
||||||
|
});
|
||||||
|
|
||||||
|
linkElement.addEventListener('mouseenter', function(event) {
|
||||||
|
clearTimeout(opacityTimeout);
|
||||||
|
clearTimeout(contentTimeout);
|
||||||
|
showTooltip(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
tooltipWrapper.addEventListener('mouseenter', function(event) {
|
||||||
|
clearTimeout(opacityTimeout);
|
||||||
|
clearTimeout(contentTimeout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('{{ include.wrapperQuerySelector }} a').forEach(setupListeners);
|
||||||
|
</script>
|
265
_includes/notes_graph.html
Normal file
265
_includes/notes_graph.html
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js"
|
||||||
|
integrity="sha512-FHsFVKQ/T1KWJDGSbrUhTJyS1ph3eRrxI228ND0EGaEp6v4a/vGwPWd3Dtd/+9cI7ccofZvl/wulICEurHN1pg=="
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<div id="zoom"></div>
|
||||||
|
<div id="graph-wrapper">
|
||||||
|
<script>
|
||||||
|
const MINIMAL_NODE_SIZE = 8;
|
||||||
|
const MAX_NODE_SIZE = 12;
|
||||||
|
const ACTIVE_RADIUS_FACTOR = 1.5;
|
||||||
|
const STROKE = 1;
|
||||||
|
const FONT_SIZE = 16;
|
||||||
|
const TICKS = 200;
|
||||||
|
const FONT_BASELINE = 40;
|
||||||
|
const MAX_LABEL_LENGTH = 50;
|
||||||
|
|
||||||
|
const graphData = {% include notes_graph.json %}
|
||||||
|
let nodesData = graphData.nodes;
|
||||||
|
let linksData = graphData.edges;
|
||||||
|
|
||||||
|
const nodeSize = {};
|
||||||
|
|
||||||
|
const updateNodeSize = () => {
|
||||||
|
nodesData.forEach((el) => {
|
||||||
|
let weight =
|
||||||
|
3 *
|
||||||
|
Math.sqrt(
|
||||||
|
linksData.filter((l) => l.source === el.id || l.target === el.id)
|
||||||
|
.length + 1
|
||||||
|
);
|
||||||
|
if (weight < MINIMAL_NODE_SIZE) {
|
||||||
|
weight = MINIMAL_NODE_SIZE;
|
||||||
|
} else if (weight > MAX_NODE_SIZE) {
|
||||||
|
weight = MAX_NODE_SIZE;
|
||||||
|
}
|
||||||
|
nodeSize[el.id] = weight;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClick = (d) => {
|
||||||
|
window.location = d.path
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseover = function (d) {
|
||||||
|
const relatedNodesSet = new Set();
|
||||||
|
linksData
|
||||||
|
.filter((n) => n.target.id == d.id || n.source.id == d.id)
|
||||||
|
.forEach((n) => {
|
||||||
|
relatedNodesSet.add(n.target.id);
|
||||||
|
relatedNodesSet.add(n.source.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
node.attr("class", (node_d) => {
|
||||||
|
if (node_d.id !== d.id && !relatedNodesSet.has(node_d.id)) {
|
||||||
|
return "inactive";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
link.attr("class", (link_d) => {
|
||||||
|
if (link_d.source.id !== d.id && link_d.target.id !== d.id) {
|
||||||
|
return "inactive";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
link.attr("stroke-width", (link_d) => {
|
||||||
|
if (link_d.source.id === d.id || link_d.target.id === d.id) {
|
||||||
|
return STROKE * 4;
|
||||||
|
}
|
||||||
|
return STROKE;
|
||||||
|
});
|
||||||
|
text.attr("class", (text_d) => {
|
||||||
|
if (text_d.id !== d.id && !relatedNodesSet.has(text_d.id)) {
|
||||||
|
return "inactive";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseout = function (d) {
|
||||||
|
node.attr("class", "");
|
||||||
|
link.attr("class", "");
|
||||||
|
text.attr("class", "");
|
||||||
|
link.attr("stroke-width", STROKE);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sameNodes = (previous, next) => {
|
||||||
|
if (next.length !== previous.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = new Map();
|
||||||
|
for (const node of previous) {
|
||||||
|
map.set(node.id, node.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of next) {
|
||||||
|
const found = map.get(node.id);
|
||||||
|
if (!found || found !== node.title) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sameEdges = (previous, next) => {
|
||||||
|
if (next.length !== previous.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const set = new Set();
|
||||||
|
for (const edge of previous) {
|
||||||
|
set.add(`${edge.source.id}-${edge.target.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const edge of next) {
|
||||||
|
if (!set.has(`${edge.source}-${edge.target}`)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const graphWrapper = document.getElementById('graph-wrapper')
|
||||||
|
const element = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
element.setAttribute("width", graphWrapper.getBoundingClientRect().width);
|
||||||
|
element.setAttribute("height", window.innerHeight * 0.8);
|
||||||
|
graphWrapper.appendChild(element);
|
||||||
|
|
||||||
|
const reportWindowSize = () => {
|
||||||
|
element.setAttribute("width", window.innerWidth);
|
||||||
|
element.setAttribute("height", window.innerHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.onresize = reportWindowSize;
|
||||||
|
|
||||||
|
const svg = d3.select("svg");
|
||||||
|
const width = Number(svg.attr("width"));
|
||||||
|
const height = Number(svg.attr("height"));
|
||||||
|
let zoomLevel = 1;
|
||||||
|
|
||||||
|
const simulation = d3
|
||||||
|
.forceSimulation(nodesData)
|
||||||
|
.force("forceX", d3.forceX().x(width / 2))
|
||||||
|
.force("forceY", d3.forceY().y(height / 2))
|
||||||
|
.force("charge", d3.forceManyBody())
|
||||||
|
.force(
|
||||||
|
"link",
|
||||||
|
d3
|
||||||
|
.forceLink(linksData)
|
||||||
|
.id((d) => d.id)
|
||||||
|
.distance(70)
|
||||||
|
)
|
||||||
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
||||||
|
.force("collision", d3.forceCollide().radius(80))
|
||||||
|
.stop();
|
||||||
|
|
||||||
|
const g = svg.append("g");
|
||||||
|
let link = g.append("g").attr("class", "links").selectAll(".link");
|
||||||
|
let node = g.append("g").attr("class", "nodes").selectAll(".node");
|
||||||
|
let text = g.append("g").attr("class", "text").selectAll(".text");
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
|
if (d3.event) {
|
||||||
|
const scale = d3.event.transform;
|
||||||
|
zoomLevel = scale.k;
|
||||||
|
g.attr("transform", scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoomOrKeep = (value) => (zoomLevel >= 1 ? value / zoomLevel : value);
|
||||||
|
|
||||||
|
const font = Math.max(Math.round(zoomOrKeep(FONT_SIZE)), 1);
|
||||||
|
|
||||||
|
text.attr("font-size", (d) => font);
|
||||||
|
text.attr("y", (d) => d.y - zoomOrKeep(FONT_BASELINE) + 8);
|
||||||
|
link.attr("stroke-width", zoomOrKeep(STROKE));
|
||||||
|
node.attr("r", (d) => {
|
||||||
|
return zoomOrKeep(nodeSize[d.id]);
|
||||||
|
});
|
||||||
|
svg
|
||||||
|
.selectAll("circle")
|
||||||
|
.filter((_d, i, nodes) => d3.select(nodes[i]).attr("active"))
|
||||||
|
.attr("r", (d) => zoomOrKeep(ACTIVE_RADIUS_FACTOR * nodeSize[d.id]));
|
||||||
|
|
||||||
|
document.getElementById("zoom").innerHTML = zoomLevel.toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ticked = () => {
|
||||||
|
node.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
|
||||||
|
text
|
||||||
|
.attr("x", (d) => d.x)
|
||||||
|
.attr("y", (d) => d.y - (FONT_BASELINE - nodeSize[d.id]) / zoomLevel);
|
||||||
|
link
|
||||||
|
.attr("x1", (d) => d.source.x)
|
||||||
|
.attr("y1", (d) => d.source.y)
|
||||||
|
.attr("x2", (d) => d.target.x)
|
||||||
|
.attr("y2", (d) => d.target.y);
|
||||||
|
};
|
||||||
|
|
||||||
|
const restart = () => {
|
||||||
|
updateNodeSize();
|
||||||
|
node = node.data(nodesData, (d) => d.id);
|
||||||
|
node.exit().remove();
|
||||||
|
node = node
|
||||||
|
.enter()
|
||||||
|
.append("circle")
|
||||||
|
.attr("r", (d) => {
|
||||||
|
return nodeSize[d.id];
|
||||||
|
})
|
||||||
|
.on("click", onClick)
|
||||||
|
.on("mouseover", onMouseover)
|
||||||
|
.on("mouseout", onMouseout)
|
||||||
|
.merge(node);
|
||||||
|
|
||||||
|
link = link.data(linksData, (d) => `${d.source.id}-${d.target.id}`);
|
||||||
|
link.exit().remove();
|
||||||
|
link = link.enter().append("line").attr("stroke-width", STROKE).merge(link);
|
||||||
|
|
||||||
|
text = text.data(nodesData, (d) => d.label);
|
||||||
|
text.exit().remove();
|
||||||
|
text = text
|
||||||
|
.enter()
|
||||||
|
.append("text")
|
||||||
|
.text((d) => shorten(d.label.replace(/_*/g, ""), MAX_LABEL_LENGTH))
|
||||||
|
.attr("font-size", `${FONT_SIZE}px`)
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("alignment-baseline", "central")
|
||||||
|
.on("click", onClick)
|
||||||
|
.on("mouseover", onMouseover)
|
||||||
|
.on("mouseout", onMouseout)
|
||||||
|
.merge(text);
|
||||||
|
|
||||||
|
node.attr("active", (d) => isCurrentPath(d.path) ? true : null);
|
||||||
|
text.attr("active", (d) => isCurrentPath(d.path) ? true : null);
|
||||||
|
|
||||||
|
simulation.nodes(nodesData);
|
||||||
|
simulation.force("link").links(linksData);
|
||||||
|
simulation.alpha(1).restart();
|
||||||
|
simulation.stop();
|
||||||
|
|
||||||
|
for (let i = 0; i < TICKS; i++) {
|
||||||
|
simulation.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
ticked();
|
||||||
|
};
|
||||||
|
|
||||||
|
const zoomHandler = d3.zoom().scaleExtent([0.2, 3]).on("zoom", resize);
|
||||||
|
|
||||||
|
zoomHandler(svg);
|
||||||
|
restart();
|
||||||
|
|
||||||
|
function isCurrentPath(notePath) {
|
||||||
|
return window.location.pathname.includes(notePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
function shorten(str, maxLen, separator = ' ') {
|
||||||
|
if (str.length <= maxLen) return str;
|
||||||
|
return str.substr(0, str.lastIndexOf(separator, maxLen)) + '...';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</div>
|
1
_includes/notes_graph.json
Normal file
1
_includes/notes_graph.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"edges":[{"source":"229320080329284392969231993","target":"50678562812897"},{"source":"229320080329284392969231993","target":"72697617314704"},{"source":"229320080329284392969231993","target":"36028"}],"nodes":[{"id":"50678562812897","path":"/hypertext","label":"Hypertext"},{"id":"72697617314704","path":"/protocols","label":"Protocols"},{"id":"36028","path":"/rss","label":"RSS"},{"id":"229320080329284392969231993","path":"/statement-of-intent","label":"Statement of Intent"}]}
|
@ -12,6 +12,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
{%- include footer.html -%}
|
{%- include footer.html -%}
|
||||||
|
{% include link-previews.html wrapperQuerySelector="content" %}
|
||||||
<script src="{{ "/assets/js/scripts.js" | relative_url }}" async></script>
|
<script src="{{ "/assets/js/scripts.js" | relative_url }}" async></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
48
_layouts/note.html
Normal file
48
_layouts/note.html
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
---
|
||||||
|
<div class="flex">
|
||||||
|
<article class="w-50">
|
||||||
|
<div>
|
||||||
|
<h1>{{ page.title }}</h1>
|
||||||
|
<time datetime="{{ page.last_modified_at | date_to_xmlschema }}">{% if page.type != 'pages' %}
|
||||||
|
Last updated on {{ page.last_modified_at | date: "%B %-d, %Y" }}
|
||||||
|
{% endif %}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="notes-entry-container">
|
||||||
|
<content>
|
||||||
|
{{ content }}
|
||||||
|
</content>
|
||||||
|
|
||||||
|
<side style="font-size: 0.9em">
|
||||||
|
<h3 style="margin-bottom: 1em">Notes mentioning this note</h3>
|
||||||
|
{% if page.backlinks.size > 0 %}
|
||||||
|
<div style="display: grid; grid-gap: 1em; grid-template-columns: repeat(1fr);">
|
||||||
|
{% for backlink in page.backlinks %}
|
||||||
|
<div class="backlink-box">
|
||||||
|
<a class="internal-link" href="{{ backlink.url | relative_url }}{%- if site.use_html_extension -%}.html{%- endif -%}">{{ backlink.title }}</a><br>
|
||||||
|
<div style="font-size: 0.9em">{{ backlink.excerpt | strip_html | truncatewords: 20 }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
There are no notes linking to this note.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</side>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div class="w-50">
|
||||||
|
|
||||||
|
<p>Here are all the notes in this garden, along with their links, visualized as a graph.</p>
|
||||||
|
|
||||||
|
{% include notes_graph.html %}
|
||||||
|
</div>
|
||||||
|
</div>
|
5
_notes/hypertext.md
Normal file
5
_notes/hypertext.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Hypertext
|
||||||
|
---
|
||||||
|
|
||||||
|
Hypertext Markup Language
|
3
_notes/protocols.md
Normal file
3
_notes/protocols.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
|
5
_notes/rss.md
Normal file
5
_notes/rss.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: RSS
|
||||||
|
---
|
||||||
|
|
||||||
|
Really Simple Syndication
|
5
_notes/statement-of-intent.md
Normal file
5
_notes/statement-of-intent.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Statement of Intent
|
||||||
|
---
|
||||||
|
|
||||||
|
Hypha’s practice is situated across many topics that are present in the theme of _Adaptive Reuse & Creative Misuse_. Drawing from our collective experiences, histories, and methodologies, our goal for the micro-residency to investigate how notions of digital infrastructure can be reused, reinterpreted, and reconfigured, to realize a kind of public space. Our approach to this theme will be composed of a few, very preliminary, subjects that will ground the residency: the situated histories of digital infrastructure, the implications of [[protocols]] for publishing ([[Hypertext]], [[RSS]], Peer-to-peer) in defining public spaces, and the possibilities of cooperative approaches to maintenance and repair. Our intent is to make the process of this investigation public through online tools mapping our thinking about the theme (Open channels in Are.na as one example) and cultivating a Digital Public Garden as part of Hypha’s contributions to the initiative (a resyndicatable adaptive online notebook). The outputs from the micro-residency will be a written contribution to the [_Field Guide to the Digital Real_](https://www.are.na/from-later/field-guide-to-the-digital-real) and a micro-website containing the synthesis of our investigations and our evolving practice. The outputs will be textual and visual, and draw from our collaborative practices as a cooperative. They will explore ways to represent relationships with existing and emergent technologies within our communities. Through our micro-residency we will capture a poetic interpretation of the theme and provide prompts for institutions in the city on how they could reconfigure technology to create radically creative platforms.
|
106
_plugins/bidirectional_links_generator.rb
Normal file
106
_plugins/bidirectional_links_generator.rb
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
class BidirectionalLinksGenerator < Jekyll::Generator
|
||||||
|
def generate(site)
|
||||||
|
graph_nodes = []
|
||||||
|
graph_edges = []
|
||||||
|
|
||||||
|
all_notes = site.collections['notes'].docs
|
||||||
|
all_pages = site.pages
|
||||||
|
|
||||||
|
all_docs = all_notes + all_pages
|
||||||
|
|
||||||
|
link_extension = !!site.config["use_html_extension"] ? '.html' : ''
|
||||||
|
|
||||||
|
# Convert all Wiki/Roam-style double-bracket link syntax to plain HTML
|
||||||
|
# anchor tag elements (<a>) with "internal-link" CSS class
|
||||||
|
all_docs.each do |current_note|
|
||||||
|
all_docs.each do |note_potentially_linked_to|
|
||||||
|
title_from_filename = File.basename(
|
||||||
|
note_potentially_linked_to.basename,
|
||||||
|
File.extname(note_potentially_linked_to.basename)
|
||||||
|
).gsub('_', ' ').gsub('-', ' ').capitalize
|
||||||
|
|
||||||
|
# Replace double-bracketed links with label using note title
|
||||||
|
# [[A note about cats|this is a link to the note about cats]]
|
||||||
|
current_note.content = current_note.content.gsub(
|
||||||
|
/\[\[#{title_from_filename}\|(.+?)(?=\])\]\]/i,
|
||||||
|
"<a class='internal-link' href='.#{note_potentially_linked_to.url}#{link_extension}'>\\1</a>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Replace double-bracketed links with label using note filename
|
||||||
|
# [[cats|this is a link to the note about cats]]
|
||||||
|
current_note.content = current_note.content.gsub(
|
||||||
|
/\[\[#{note_potentially_linked_to.data['title']}\|(.+?)(?=\])\]\]/i,
|
||||||
|
"<a class='internal-link' href='.#{note_potentially_linked_to.url}#{link_extension}'>\\1</a>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Replace double-bracketed links using note title
|
||||||
|
# [[a note about cats]]
|
||||||
|
current_note.content = current_note.content.gsub(
|
||||||
|
/\[\[(#{note_potentially_linked_to.data['title']})\]\]/i,
|
||||||
|
"<a class='internal-link' href='.#{note_potentially_linked_to.url}#{link_extension}'>\\1</a>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Replace double-bracketed links using note filename
|
||||||
|
# [[cats]]
|
||||||
|
current_note.content = current_note.content.gsub(
|
||||||
|
/\[\[(#{title_from_filename})\]\]/i,
|
||||||
|
"<a class='internal-link' href='.#{note_potentially_linked_to.url}#{link_extension}'>\\1</a>"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# At this point, all remaining double-bracket-wrapped words are
|
||||||
|
# pointing to non-existing pages, so let's turn them into disabled
|
||||||
|
# links by greying them out and changing the cursor
|
||||||
|
current_note.content = current_note.content.gsub(
|
||||||
|
/\[\[(.*)\]\]/i, # match on the remaining double-bracket links
|
||||||
|
<<~HTML.chomp # replace with this HTML (\\1 is what was inside the brackets)
|
||||||
|
<span title='There is no note that matches this link.' class='invalid-link'>
|
||||||
|
<span class='invalid-link-brackets'>[[</span>
|
||||||
|
\\1
|
||||||
|
<span class='invalid-link-brackets'>]]</span></span>
|
||||||
|
HTML
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Identify note backlinks and add them to each note
|
||||||
|
all_notes.each do |current_note|
|
||||||
|
# Nodes: Jekyll
|
||||||
|
notes_linking_to_current_note = all_notes.filter do |e|
|
||||||
|
e.content.include?(current_note.url)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Nodes: Graph
|
||||||
|
graph_nodes << {
|
||||||
|
id: note_id_from_note(current_note),
|
||||||
|
path: "#{current_note.url}#{link_extension}",
|
||||||
|
label: current_note.data['title'],
|
||||||
|
} unless current_note.path.include?('_notes/index.html')
|
||||||
|
|
||||||
|
# Edges: Jekyll
|
||||||
|
current_note.data['backlinks'] = notes_linking_to_current_note
|
||||||
|
|
||||||
|
# Edges: Graph
|
||||||
|
notes_linking_to_current_note.each do |n|
|
||||||
|
graph_edges << {
|
||||||
|
source: note_id_from_note(n),
|
||||||
|
target: note_id_from_note(current_note),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
File.write('_includes/notes_graph.json', JSON.dump({
|
||||||
|
edges: graph_edges,
|
||||||
|
nodes: graph_nodes,
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
def note_id_from_note(note)
|
||||||
|
note.data['title']
|
||||||
|
.dup
|
||||||
|
.gsub(/\W+/, ' ')
|
||||||
|
.delete(' ')
|
||||||
|
.to_i(36)
|
||||||
|
.to_s
|
||||||
|
end
|
||||||
|
end
|
18
_plugins/empty_front_matter_note_injector.rb
Normal file
18
_plugins/empty_front_matter_note_injector.rb
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
EMPTY_FRONT_MATTER = <<~JEKYLL
|
||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
JEKYLL
|
||||||
|
|
||||||
|
# Inject empty front matter in notes that don't have any
|
||||||
|
Jekyll::Hooks.register :site, :after_init do |site|
|
||||||
|
Dir.glob(site.collections['notes'].relative_directory + '/**/*.md').each do |filename|
|
||||||
|
raw_note_content = File.read(filename)
|
||||||
|
unless raw_note_content.start_with?('---')
|
||||||
|
raw_note_content.prepend(EMPTY_FRONT_MATTER)
|
||||||
|
File.write(filename, raw_note_content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -5,7 +5,7 @@ title: "Initial Seeds"
|
|||||||
|
|
||||||
Set of areas that guide our reveries?
|
Set of areas that guide our reveries?
|
||||||
|
|
||||||
1. The history of hypertext, rss+adjacent protocols and standards.
|
1. The history of [hypertext](/hypertext), rss+adjacent protocols and standards.
|
||||||
2. The act of publishing as "making something public" → publicness → hybrid public space.
|
2. The act of publishing as "making something public" → publicness → hybrid public space.
|
||||||
3. The possibilities for *the infrastructural* (maintenance/repair) to draw from the past to rethink the present through co-operative approaches.
|
3. The possibilities for *the infrastructural* (maintenance/repair) to draw from the past to rethink the present through co-operative approaches.
|
||||||
|
|
||||||
|
@ -6,3 +6,70 @@
|
|||||||
.no-underline-hover:focus {
|
.no-underline-hover:focus {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notes Graph
|
||||||
|
|
||||||
|
.links line {
|
||||||
|
stroke: $accent-color;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes circle {
|
||||||
|
cursor: pointer;
|
||||||
|
fill: $accent-color;
|
||||||
|
transition: all 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text text {
|
||||||
|
cursor: pointer;
|
||||||
|
fill: $accent-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes [active],
|
||||||
|
.text [active] {
|
||||||
|
cursor: pointer;
|
||||||
|
fill: $accent-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inactive {
|
||||||
|
opacity: 0.1;
|
||||||
|
transition: all 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#graph-wrapper {
|
||||||
|
border-radius: 4px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link previews
|
||||||
|
|
||||||
|
content a.internal-link {
|
||||||
|
background-color: rgba($accent-color, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#tooltip-wrapper {
|
||||||
|
background: white;
|
||||||
|
padding: 1em;
|
||||||
|
border: 1px solid $accent-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
width: 400px;
|
||||||
|
height: 250px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
box-shadow: 0 5px 10px rgba(0,0,0,0.1);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 100ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tooltip-wrapper:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background-image: linear-gradient(to bottom, rgba(255,255,255, 0), rgba(255,255,255, 1) 90%);
|
||||||
|
width: 100%;
|
||||||
|
height: 75px;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user