1
1
mirror of https://github.com/XXIIVV/webring.git synced 2024-07-14 15:01:20 +03:00

Closed Hallway/Wiki

This commit is contained in:
neauoire 2020-05-19 19:43:25 +09:00
parent a4a4d64dd9
commit 50afc43c23
11 changed files with 0 additions and 1640 deletions

View File

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="links/fonts.css"/>
<link rel="stylesheet" type="text/css" href="links/main.css"/>
<script type="text/javascript">const module = { exports:{} }</script>
<script src="scripts/hallway.js"></script>
<script src="scripts/sites.js"></script>
<title>Hallway</title>
</head>
<body>
<script>
const hallway = new Hallway(sites);
hallway.install(document.body);
hallway.start();
</script>
</body>
</html>

View File

@ -1,232 +0,0 @@
'use strict'
const reChannel = /(\s|^)\/([a-zA-Z0-9]+)/g
const reUser = /((\s|^)@&lt;[a-zA-Z0-9./:\-_+~#= ]*&gt;)/g
const reTag = /(^|\s)(#[a-z\d-]+)/ig
const reUrl = /((https?):\/\/(([-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b)([-a-zA-Z0-9@:%_+.~#?&//=]*)))/g
function Hallway (sites) {
const feeds = {}
this.sites = sites
this._el = document.createElement('div')
this._el.id = 'hallway'
const filterAndPage = window.location.hash.split('^')
this.finder = { filter: stripHash(filterAndPage[0]), page: filterAndPage[1] || 1 }
this.cache = null
this._main = document.createElement('main')
this._main.id = 'entries'
this._footer = document.createElement('footer')
this._footer.id = 'footer'
this.readme = '<p>The <strong>Hallway</strong> is a decentralized forum, to join the conversation, add a <a href="https://github.com/XXIIVV/webring#joining-the-hallway">feed:</a> field to your entry in the <a href="https://github.com/XXIIVV/Webring/">webring</a>.</p>'
this.nav = '<p class="buttons"><a href="https://github.com/XXIIVV/webring">Information</a> | <a href="index.html">The Portal</a> | <a href="wiki.html">The Wiki</a> | <a id="opml">OPML</a></p>'
this._footer.innerHTML = `${this.readme}${this.nav}`
this.install = function (host) {
this._el.appendChild(this._main)
this._el.appendChild(this._footer)
host.appendChild(this._el)
this.findFeeds()
}
this.start = function () {
this._main.innerHTML = 'Loading..'
this.fetchFeeds()
}
this.refresh = function (feeds = this.cache) {
const entries = this.findEntries(feeds)
const localEntries = entries.filter(entry => this.filterExternals(entry))
const channels = this.find(localEntries, 'channel')
const users = this.find(localEntries, 'author')
const tags = this.findTags(localEntries)
const relevantEntries = localEntries
.filter(entry => !this.finder.filter || (entry.author === this.finder.filter || entry.channel === this.finder.filter || entry.tags.includes(this.finder.filter) || entry.body.includes(this.finder.filter)))
const mainHtml = `
<div><ul>
${relevantEntries.filter((_, id) => id < Number(this.finder.page) * 20 && id >= (Number(this.finder.page) - 1) * 20).reduce((acc, val) => acc + this.templateEntry(val) + '\n', '')}
</ul>
<div id='pagination'>
${[...Array(Math.ceil(relevantEntries.length / 20)).keys()].reduce((acc, num) => `${acc}<span class='${Number(hallway.finder.page) === num + 1 ? 'selected' : ''}' onclick='filter("${this.finder.filter}^${num + 1}")'>${num + 1}</span>`, '')}
</div>
<a id='showbar' onclick="toggleVisibility('aside');"></a></div>`
const aside = `
<aside id='aside'>
<a id='hidebar' onclick="toggleVisibility('aside');"></a>
<ul id='channels'>
<li onclick='filter("")' class='${hallway.finder.filter === '' ? 'selected' : ''}'><a href='#'>hallway <span class='right'>${localEntries.length}</span></a></li>
${Object.keys(channels).slice(0, 15).reduce((acc, val) => acc + `<li onclick='filter("${val}")' class='${hallway.finder.filter === val ? 'selected' : ''}'><a href='#${val}'>${val} <span class='right'>${channels[val]}</span></a></li>\n`, '')}
</ul>
<ul id='users'>
${Object.keys(users).slice(0, 15).reduce((acc, val) => acc + `<li onclick='filter("${val}")' class='${hallway.finder.filter === val ? 'selected' : ''}' href='#${val}'>${val} <span class='right'>${users[val]}</span></li>\n`, '')}
</ul>
<ul id='tags'>
${Object.keys(tags).slice(0, 15).reduce((acc, val) => acc + `<li onclick='filter("${val}")' class='${hallway.finder.filter === val ? 'selected' : ''}' href='#${val}'>#${val} <span class='right'>${tags[val]}</span></li>\n`, '')}
</ul>
</aside>`
this._main.innerHTML = `${mainHtml}${aside}`
if (feeds) {
this.cache = feeds
}
}
// Entries
this.filterExternals = function (entry) {
const matches = entry.body.match(reUser)
const locals = Object.keys(feeds)
return matches ? matches.some(match => locals.some(local => match.indexOf(feeds[local].path) !== -1)) : true
}
this.find = function (entries, key) {
const h = {}
for (const entry of entries) {
if (entry && entry[key]) {
h[entry[key]] = h[entry[key]] ? h[entry[key]] + 1 : 1
}
}
return h
}
this.findTags = function (entries) {
const tags = {}
for (const entry of entries) {
entry.tags.map(tag => {
tags[tag] = tags[tag] ? tags[tag] + 1 : 1
})
}
return tags
}
this.findEntries = function (feeds) {
const a = []
for (const id in feeds) {
for (const i in feeds[id].content) {
a.push(feeds[id].content[i])
}
}
return a.sort((a, b) => a.offset - b.offset)
}
this.findMention = function (found) {
const mention = Object.keys(feeds).filter(user => found.indexOf(feeds[user].path) > -1)
return ` <span class='user local'>${mention[0]}</span>`
}
this.templateEntry = function (entry) {
entry.html = entry.body
.replace(reChannel, '$1<span class="channel">/$2</span>')
.replace(reUser, this.findMention)
.replace(reTag, '$1<span class="tag">$2</span>')
.replace(reUrl, '<a target="_blank" href="$1">$1</a>')
const filter = window.location.hash.substr(1).replace(/\+/g, ' ').toLowerCase()
const highlight = filter === entry.author.toLowerCase()
const origin = feeds[entry.author].path
return `<li class='entry ${highlight ? 'highlight' : ''}'><span class='date'>${timeAgo(Date.parse(entry.date))}</span> <a class='author' href='${origin}' target='_blank'>${entry.author}</a> <span class='body'>${entry.html}</span></li>`
}
// Feeds
this.findFeeds = function () {
console.log('Finding feeds..')
for (const site of sites) {
if (site.feed && site.author) {
feeds[site.author] = { path: site.feed }
}
}
console.log(`Found ${Object.keys(feeds).length} feeds.`)
}
this.fetchFeeds = function () {
console.log(`Fetching ${Object.keys(feeds).length} feeds..`)
for (const id in feeds) {
this.fetchFeed(id, feeds[id])
}
}
this.fetchFeed = function (id, feed) {
console.log(`Fetching ${id}(${feed.path})..`)
fetch(feed.path, { cache: 'no-store' }).then(x => x.text()).then((content) => {
feeds[id].content = parseFeed(id, content)
this.refresh(feeds)
}).catch((err) => {
console.warn(`${id}`, err)
})
}
// Utils
function parseFeed (author, feed) {
const lines = feed.split('\n').filter((line) => line.substr(0, 1) !== '#')
const entries = []
for (const id in lines) {
const line = lines[id].trim()
if (line === '') { continue }
const parts = line.replace(' ', '\t').split('\t')
const date = parts[0].trim()
const body = escapeHtml(parts[1].trim()).trim()
const channel = body.substr(0, 1) === '/' ? cleanTags(body.split(' ')[0].substr(1).toLowerCase()) : body.substr(0, 1) === '@' ? 'veranda' : 'lobby'
const tags = (body.match(reTag) || []).map(a => cleanTags(a.substr(a.indexOf('#') + 1).toLowerCase()))
const offset = new Date() - new Date(date)
entries.push({ date, body, author, offset, channel, tags })
}
return entries
}
function timeAgo (dateParam) {
const date = typeof dateParam === 'object' ? dateParam : new Date(dateParam)
const today = new Date()
const yesterday = new Date(today - 86400000)
const seconds = Math.round((today - date) / 1000)
const minutes = Math.round(seconds / 60)
const isToday = today.toDateString() === date.toDateString()
const isYesterday = yesterday.toDateString() === date.toDateString()
if (seconds < 5) {
return 'just now'
} else if (seconds < 60) {
return `${seconds} seconds ago`
} else if (seconds < 90) {
return 'a minute ago'
} else if (minutes < 60) {
return `${minutes} minutes ago`
} else if (isToday) {
return `${Math.floor(minutes / 60)} hours ago`
} else if (isYesterday) {
return 'yesterday'
}
return `${Math.floor(minutes / 1440)} days ago`
}
}
function toggleVisibility (id) {
const e = document.getElementById(id)
if (e.style.display === 'block') { e.style.display = 'none' } else { e.style.display = 'block' }
}
function escapeHtml (unsafe) {
return unsafe.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;')
}
function cleanTags (unsafe) {
return unsafe.replace(/([^a-z0-9]+)/gi, '-')
}
function stripHash (hash) {
const decoded = decodeURIComponent(hash)
return decoded.charAt(0) === '#' ? decoded.substring(1) : decoded
}
function filter (name) {
window.location.hash = name
const filterAndPage = window.location.hash.split('^')
hallway.finder = { filter: stripHash(filterAndPage[0]), page: filterAndPage[1] || 1 }
hallway.refresh()
}

View File

@ -1,57 +0,0 @@
'use strict'
function indental (data, Type) {
function formatLine (line) {
const a = []
const h = {}
for (const child of line.children) {
if (child.key) {
if (h[child.key]) { console.warn(`Redefined key: ${child.key}.`) }
h[child.key] = child.value
} else if (child.children.length === 0 && child.content) {
a[a.length] = child.content
} else {
h[child.content.toUpperCase()] = formatLine(child)
}
}
return a.length > 0 ? a : h
}
function makeLine (line) {
return line.indexOf(' : ') > -1 ? {
indent: line.search(/\S|$/),
content: line.trim(),
key: line.split(' : ')[0].trim().toUpperCase(),
value: line.split(' : ')[1].trim()
} : {
indent: line.search(/\S|$/),
content: line.trim(),
children: []
}
}
function skipLine (line) {
return line.trim() !== '' && line.trim().substr(0, 1) !== ';' && line.trim().slice(-1) !== '`'
}
const lines = data.split('\n').filter(skipLine).map(makeLine)
// Assoc
const stack = {}
for (const line of lines) {
const target = stack[line.indent - 2]
if (target) { target.children.push(line) }
stack[line.indent] = line
}
// Format
const h = {}
for (const id in lines) {
const line = lines[id]
if (line.indent > 0) { continue }
const key = line.content.toUpperCase()
if (h[key]) { console.warn(`Redefined key: ${key}, line ${id}.`) }
h[key] = Type ? new Type(key, formatLine(line)) : formatLine(line)
}
return h
}

View File

@ -1,13 +0,0 @@
const outlines = sites.filter(site => site.rss).map(site => {
return `<outline type="rss" xmlUrl="${site.rss}" title="${site.title}"/>`
})
const template = `<?xml version="1.0" encoding="UTF-8"?><opml version="1.1"><head></head><body>${outlines.join('')}</body></opml>`
window.addEventListener('DOMContentLoaded', () => {
const a = document.getElementById('opml')
if (a) {
a.setAttribute('href', 'data:text/plain;charset=utf-8,' + window.encodeURIComponent(template))
a.setAttribute('download', 'webring.opml')
}
})

View File

@ -1,121 +0,0 @@
'use strict'
function Portal (sites) {
this.el = document.createElement('div')
this.el.id = 'portal'
this.sites = sites
// Templates
const _readme = '<p class="readme">This webring is an attempt to inspire artists & developers to build their own website and share traffic among each other. The ring welcomes personalized websites such as <b>diaries, wikis & portfolios</b>.</p><p>To add yourself to the ring, submit a <a href="https://github.com/XXIIVV/webring/edit/master/scripts/sites.js" target="_blank">Pull Request</a>.<br />If you found a broken link, please <a href="https://github.com/XXIIVV/webring/issues/new" target="_blank">report it</a>.</p>'
const _buttons = '<p class="buttons"><a href="#random" onClick="portal.reload()">Random</a> | <a href="https://github.com/XXIIVV/webring">Information</a> <a id="icon" href="#random" onClick="portal.reload()"></a> | <a href="hallway.html">The Hallway</a> | <a href="wiki.html">The Wiki</a> | <a id="opml">OPML</a></p>'
function _directory (sites) {
const siteTypesArray = [...new Set(sites.map(site => site.type).filter(Boolean))]
const siteLangsArray = [...new Set(sites
.reduce((flattened, site) => flattened.concat(site.langs), [])
.sort())]
const siteTypes = siteTypesArray.reduce((output, siteType) =>
`${output}<a data-type="${siteType}" href="#" onclick="filterSites(event)">&lt;${siteType}&gt;</a>`,
'<a class="currentType" data-type="all" href="#" onclick="filterSites(event)">&lt;all&gt;</a>')
const siteLangs = siteLangsArray.reduce((output, siteLang) =>
`${output}<a data-lang="${siteLang}" href="#" onclick="filterSites(event)">&lt;${siteLang}&gt;</a>`,
'<a class="currentLang" data-lang="all" href="#" onclick="filterSites(event)">&lt;all&gt;</a>')
const listItems = sites.reduce((acc, site, id) =>
`${acc}<li ${_type(site)} ${_lang(site)} id='${id}'><a href='${site.url}'>${_name(site)}</a></li>`, '')
return `<nav>${siteTypes}</nav><nav>${siteLangs}</nav><main><ul>${listItems}</ul></main><footer>${_readme}${_buttons}</footer>`
}
function _name (site) {
return site.title || `${site.url.split('//')[1].replace(/\/+$/, '')}`
}
function _type (site) {
return site.type ? `data-type="${site.type} "` : ''
}
function _lang (site) {
return `data-lang="${site.langs.join(' ').trim()}"`
}
function _redirectView (site) {
return `<main><p>Redirecting to <a href="${site.url}">${site.url}</a></p></main>
<footer><p class='buttons'><a href='#' onClick="portal.reload('')">Directory</a> | <a href='#${site.url}' onClick="portal.reload()">Skip</a> | <a href='#random' onClick="portal.reload()">Random</a> | <a href='https://github.com/XXIIVV/webring'>Information</a> <a id='icon' href='#random' onClick="portal.reload()"></a></p>`
}
//
this.install = function (host) {
host.appendChild(this.el)
}
this.start = function () {
const hash = window.location.hash
if (hash && hash.length > 4) {
const site = this.getSite(hash.replace('#', '').trim())
this.el.innerHTML = _redirectView(site)
setTimeout(() => { window.location = site.url }, 3000)
} else {
this.el.innerHTML = _directory(this.sites)
}
}
this.reload = function () {
setTimeout(() => { window.location.reload() }, 500)
}
this.navigate = function (target) {
setTimeout(() => { window.location.href = target }, 3000)
}
this.navLangs = [...new Set(navigator.languages.map(lang => lang.split('-').shift()))]
this.sitesMatchingLangs = this.sites
.filter(site => site.langs && site.langs.some(lang => this.navLangs.includes(lang)))
this.nextSiteIndex = (ceiling, index) => index === ceiling ? 0 : index + 1
this.randomSite = this.sitesMatchingLangs[Math.floor(Math.random() * this.sitesMatchingLangs.length)]
this.getSite = function (hash) {
if (hash === 'random') {
return this.randomSite
} else {
const index = this.sitesMatchingLangs.findIndex(site => site.url.includes(hash))
return (index > -1)
? this.sitesMatchingLangs[this.nextSiteIndex(this.sitesMatchingLangs.length, index)]
: this.randomSite
}
}
}
function filterSites (e) {
e.preventDefault()
const navLink = e.target
const currentFilter = navLink.dataset.lang ? 'Lang' : 'Type'
document.querySelector(`nav a.current${currentFilter}`).classList.remove(`current${currentFilter}`)
navLink.classList.add(`current${currentFilter}`)
const typeFilter = document.querySelector('nav a.currentType').dataset.type
const langFilter = document.querySelector('nav a.currentLang').dataset.lang
const sites = document.querySelectorAll('main li')
sites.forEach(el => { el.style.display = 'block' })
sites.forEach(el => {
if (typeFilter !== 'all') {
if (el.dataset.type) {
if (el.dataset.type.trim() !== typeFilter) el.style.display = 'none'
} else {
el.style.display = 'none'
}
}
if (langFilter !== 'all') {
if (!el.dataset.lang.includes(langFilter)) el.style.display = 'none'
}
})
}

View File

@ -1,41 +0,0 @@
'use strict'
const Progress = keys => {
const stack = new Set(keys.filter(key => key))
const loaded = new Set()
const failed = new Set()
this.ratio = () => {
return loaded.size / stack.size
}
this.percentage = () => {
return (100 * this.ratio()).toFixed(2)
}
const render = () => `
<div class='${loaded.size !== stack.size ? 'busy' : 'done'}'>
<p>
${'|'.repeat(this.ratio() * 10)}
${loaded.size !== stack.size ? this.percentage() + '% ' : ''}
${loaded.size === stack.size ? 'Complete' : 'Loading'}
${failed.size !== 0 ? `(${failed.size} failed)` : ''}
</p>
</div>`
const didLoad = (key, success) => new Promise((resolve, reject) => {
if (stack.has(key)) {
loaded.add(key)
if (!success) failed.add(key)
return resolve()
} else {
return reject()
}
})
return {
success: key => didLoad(key, true),
failure: key => didLoad(key, false),
toString: render
}
}

View File

@ -1,924 +0,0 @@
'use strict'
const sites = [
{
author: 'neauoire',
contact: 'aliceffekt@gmail.com',
feed: 'https://wiki.xxiivv.com/twtxt.txt',
langs: ['en'],
rss: 'https://wiki.xxiivv.com/links/rss.xml',
title: 'xxiivv',
type: 'wiki',
url: 'https://wiki.xxiivv.com',
wiki: 'https://wiki.xxiivv.com/src/database/glossary.ndtl'
},
{
langs: ['en'],
url: 'http://estevancarlos.com'
},
{
author: 'rho',
feed: 'https://electro.pizza/twtxt.txt',
langs: ['en'],
title: 'electro pizza',
type: 'blog',
rss: 'https://electro.pizza/feed.xml',
url: 'https://electro.pizza'
},
{
author: 'joshavanier',
contact: 'joshavanier@protonmail.com',
feed: 'https://avanier.now.sh/tw.txt',
langs: ['en'],
title: 'Arachne',
type: 'wiki',
url: 'https://avanier.now.sh'
},
{
author: 'kaemura',
contact: 'chimera1190@gmail.com',
feed: 'https://kaemura.com/twttxt.txt',
langs: ['en'],
title: 'kaemura.com',
type: 'blog',
url: 'http://kaemura.com'
},
{
author: 'slisne',
contact: '@slisne@merveilles.town',
feed: 'https://liamcooke.com/merveilles/tw.txt',
langs: ['en'],
title: 'liamcooke.com',
rss: 'https://liamcooke.com/feed.xml',
url: 'https://liamcooke.com',
wiki: 'https://liamcooke.com/merveilles/wiki.ndtl'
},
{
langs: ['en'],
url: 'https://hraew.autophagy.io'
},
{
langs: ['en'],
url: 'http://evenunto.net'
},
{
langs: ['en'],
url: 'https://anxl.faith'
},
{
author: 'xvw',
contact: 'xaviervdw@gmail.com',
langs: ['fr'],
feed: 'https://xvw.github.io/twtxt/hallway.txt',
rss: 'https://xvw.github.io/atom.xml',
title: 'planet',
type: 'hybrid',
url: 'https://xvw.github.io'
},
{
langs: ['en', 'el'],
url: 'https://heracl.es'
},
{
langs: ['en'],
url: 'https://turelio.github.io'
},
{
author: 'lectronice',
contact: 'i.love.spam@lectronice.com',
feed: 'https://lectronice.com/hallway/twtxt.txt',
langs: ['en'],
title: 'shards',
type: 'blog',
url: 'https://shards.lectronice.com'
},
{
langs: ['en'],
url: 'https://craze.co.uk'
},
{
langs: ['en'],
url: 'https://shaneckel.com'
},
{
author: 'cblgh',
contact: 'cabal://cblgh.org',
feed: 'https://cblgh.org/twtxt.txt',
langs: ['en'],
title: 'cblgh.org',
url: 'https://cblgh.org'
},
{
author: 'afk',
contact: 'afk@ellugar.co',
langs: ['en'],
rss: 'http://feeds.ellugar.co/ellugar-logs',
title: 'ellugar',
type: 'wiki',
url: 'https://ellugar.co'
},
{
author: 'Cameron Higby-Naquin',
contact: 'me@chigby.org',
langs: ['en'],
title: 'chigby.org',
type: 'portfolio',
url: 'http://chigby.org'
},
{
langs: ['en'],
url: 'https://longest.voyage'
},
{
author: 'Paloma Kop',
contact: 'doubleobelisk@gmail.com',
langs: ['en'],
title: 'palomakop.tv',
type: 'portfolio',
url: 'https://palomakop.tv'
},
{
langs: ['en'],
url: 'https://v-os.ca'
},
{
langs: ['en'],
url: 'https://jmandel.xyz'
},
{
author: 'jda0',
contact: 'hi@2d4.dev',
feed: 'https://2d4.dev/tw.txt',
langs: ['en'],
title: '2d4.dev',
url: 'https://2d4.dev',
wiki: 'https://2d4.dev/wiki.ndtl'
},
{
langs: ['en'],
url: 'https://nathanwentworth.co'
},
{
langs: ['en'],
url: 'https://uonai.space'
},
{
langs: ['en'],
url: 'http://controls.ee'
},
{
langs: ['en'],
url: 'https://wasin.io'
},
{
langs: ['en'],
url: 'https://inns.studio'
},
{
langs: ['en'],
url: 'http://kokorobot.ca'
},
{
author: 'jrc03c',
contact: 'josh@ameyama.com',
langs: ['en'],
title: 'ameyama',
type: 'blog',
rss: 'https://ameyama.com/blog/rss.xml',
url: 'https://ameyama.com',
wiki: 'https://ameyama.com/data/wiki.ndtl'
},
{
author: 'wakest',
contact: '@liaizon@wake.st',
feed: 'https://wake.st/twtxt.txt',
langs: ['en'],
title: 'wake.st',
url: 'https://wake.st'
},
{
langs: ['en'],
url: 'https://xarene.la'
},
{
langs: ['en'],
url: 'https://alex.zyzhang.me'
},
{
langs: ['en'],
url: 'http://bildwissenschaft.vortok.info'
},
{
langs: ['en'],
url: 'https://jakofranko.github.com'
},
{
langs: ['en'],
url: 'https://aeriform.io'
},
{
langs: ['en', 'de'],
url: 'http://blog.lucasdidthis.com'
},
{
author: 'npisanti',
contact: 'nicola@npisanti.com',
langs: ['en'],
rss: 'http://npisanti.com/journal/rss.xml',
title: 'npisanti.com',
url: 'http://npisanti.com'
},
{
langs: ['en'],
url: 'https://underscorediscovery.ca'
},
{
author: 'drisc',
contact: 'cory@drisc.io',
feed: 'https://drisc.io/hallway/twtxt.txt',
langs: ['en'],
title: 'drisc.io',
type: 'wiki',
url: 'https://drisc.io'
},
{
langs: ['en'],
url: 'https://ricky.codes'
},
{
author: 'Marshall Bowers',
contact: 'elliott.codes@gmail.com',
langs: ['en'],
title: 'maxdeviant.com',
type: 'hybrid',
url: 'https://maxdeviant.com'
},
{
langs: ['en'],
url: 'https://tynandebold.com'
},
{
langs: ['en'],
url: 'http://gytis.co'
},
{
langs: ['en'],
url: 'https://nomand.co'
},
{
langs: ['en'],
url: 'http://memoriata.com'
},
{
langs: ['en'],
url: 'https://mmm.s-ol.nu'
},
{
contact: 'ubuwaits@gmail.com',
langs: ['en'],
rss: 'https://chad.is/rss.xml',
url: 'https://chad.is'
},
{
langs: ['en'],
url: 'https://smidgeo.com/bots'
},
{
langs: ['en', 'ru'],
url: 'https://iko.soy'
},
{
langs: ['en'],
url: 'http://atelieroilandsugar.com'
},
{
langs: ['en'],
url: 'https://magoz.is'
},
{
author: 'Szymon Kaliski',
langs: ['en'],
rss: 'https://szymonkaliski.com/feed.xml',
type: 'hybrid',
url: 'https://szymonkaliski.com'
},
{
author: 'setphen',
feed: 'https://phse.net/twtxt/merv.txt',
langs: ['en'],
rss: 'https://phse.net/post/index.xml',
title: 'phse.net',
type: 'blog',
url: 'https://phse.net'
},
{
author: 'rosano',
langs: ['en'],
url: 'https://rosano.ca',
wiki: 'https://rosano.ca/wiki.ndtl'
},
{
langs: ['en'],
url: 'https://soyboysky.github.io'
},
{
langs: ['en'],
url: 'https://gndclouds.cc'
},
{
langs: ['en', 'fr'],
url: 'https://xuv.be'
},
{
langs: ['en', 'zh'],
url: 'https://dsdshcym.github.io'
},
{
author: 'ckipp',
contact: 'ckipp@pm.me',
langs: ['en'],
title: 'chris-kipp.io',
type: 'hybrid',
url: 'https://chris-kipp.io'
},
{
langs: ['en'],
url: 'https://boffosocko.com'
},
{
author: 'kodedninja',
contact: 'karamanhunor@pm.me',
feed: 'https://t.seed.hex22.org/twtxt.txt',
langs: ['en'],
title: 'hex22',
url: 'https://hex22.org'
},
{
langs: ['en'],
url: 'https://patrikarvidsson.com'
},
{
langs: ['en'],
url: 'https://sophieleetmaa.com'
},
{
langs: ['en'],
url: 'https://xinniw.github.io'
},
{
author: 'm',
contact: 'mboxxed@gmail.com',
feed: 'https://mboxed.github.io/sodatsu/tw.txt',
langs: ['en'],
title: 'sodatsu',
type: 'wiki',
url: 'https://mboxed.github.io/sodatsu'
},
{
langs: ['en'],
url: 'https://letters.vexingworkshop.com'
},
{
author: 'tomupom',
contact: 'tom@tomhackshaw.com',
langs: ['en'],
title: 'Tom Hackshaw',
type: 'portfolio',
url: 'https://tom.org.nz',
feed: 'https://a.tom.org.nz/twtxt.txt',
wiki: 'https://a.tom.org.nz/glossary.ndtl'
},
{
title: 'Teknari',
langs: ['en'],
url: 'https://teknari.com',
author: 'teknari',
contact: 'teknari@teknari.com',
rss: 'https://teknari.com/feed.xml'
},
{
author: 'clic',
contact: 'https://t.me/clic_laplata',
langs: ['es'],
title: 'Colectivo de Livecoders',
type: 'blog',
url: 'https://colectivo-de-livecoders.gitlab.io'
},
{
langs: ['es'],
title: 'madewithtea.com',
type: 'blog',
url: 'https://www.madewithtea.com'
},
{
author: 'amorris',
feed: 'https://feed.amorris.ca/hallway.txt',
langs: ['en'],
title: 'amorris',
type: 'blog',
url: 'https://amorris.ca',
wiki: 'https://wiki.amorris.ca/glossary.ndtl'
},
{
langs: ['en'],
title: 'miha-co',
type: 'portfolio',
url: 'http://www.miha-co.ca'
},
{
author: 'buzzert',
langs: ['en'],
title: 'buzzert.net',
type: 'blog',
url: 'https://buzzert.net'
},
{
author: 'stuartpb',
contact: 's@stuartpb.com',
feed: 'https://twtxt.stuartpb.com/xxiivv.txt',
langs: ['en'],
title: 'notes.stuartpb.com',
type: 'wiki',
url: 'https://notes.stuartpb.com/'
},
{
author: 'serocell',
contact: 'psignal@s900.net',
feed: 'https://xxiii.co/twtxt.txt',
langs: ['en'],
rss: 'https://serocell.com/feeds/serocell.xml',
title: 'xxiii',
type: 'portfolio',
url: 'https://xxiii.co'
},
{
author: 'kormyen',
contact: 'h@kor.nz',
feed: 'https://kor.nz/twtxt.txt',
langs: ['en'],
title: 'kor',
type: 'wiki',
url: 'https://kor.nz',
wiki: 'https://kor.nz/db/glossary.ndtl'
},
{
author: 'quite',
contact: 'quite@hack.org',
feed: 'https://lublin.se/twtxt.txt',
langs: ['en'],
url: 'https://lublin.se'
},
{
author: 'zanneth',
contact: 'root@zanneth.com',
langs: ['en'],
title: 'zanneth.com',
type: 'blog',
url: 'https://zanneth.com'
},
{
author: 'eli_oat',
contact: 'hi@eli.li',
feed: 'https://txt.eli.li/twtxt/twtxt.txt',
langs: ['en'],
rss: 'https://eli.li/feed.rss',
title: 'eli.li',
type: 'blog',
url: 'https://eli.li'
},
{
author: 'gueorgui',
contact: 'hello@gueorgui.net',
langs: ['en'],
title: 'Gueorgui Tcherednitchenko',
type: 'portfolio',
url: 'https://gueorgui.net'
},
{
author: 'Tate Carson',
contact: 'tatecarson@gmail.com',
langs: ['en'],
title: 'Tate Carson',
type: 'portfolio',
url: 'https://www.tatecarson.com'
},
{
author: 'azlen',
contact: 'webring@azlen.me',
feed: 'https://azlen.me/twtxt.txt',
langs: ['en'],
title: 'azlen.me',
type: 'wiki',
url: 'https://azlen.me',
wiki: 'https://azlen.me/glossary.ndtl'
},
{
author: 'vega',
contact: 'vegac@protonmail.com',
feed: 'https://opinionatedguide.github.io/twtxt.txt',
langs: ['en'],
title: 'OpGuides',
url: 'https://opinionatedguide.github.io/'
},
{
author: 'cmaughan',
contact: 'mornymorny@gmail.com',
feed: 'https://chrismaughan.com/twtxt.txt',
langs: ['en'],
title: 'CMaughan',
url: 'https://chrismaughan.com/'
},
{
author: 'RaelZero',
contact: 'gaz.gong@gmail.com',
title: 'oddworlds soliloquy',
langs: ['en', 'es'],
type: 'blog',
url: 'https://oddworlds.org/'
},
{
author: 'Fundor 333',
contact: 'blog@fundor333.com',
feed: 'https://fundor333.com/twtxt.txt',
langs: ['en'],
title: 'Fundor333',
type: 'blog',
url: 'https://fundor333.com/'
},
{
contact: 'm@cass.si',
langs: ['en'],
url: 'https://cass.si'
},
{
author: 'dcb',
contact: 'dotcomboom@somnolescent.net',
feed: 'https://dotcomboom.somnolescent.net/twtxt.txt',
langs: ['en'],
title: 'somnolescent.net',
url: 'https://dotcomboom.somnolescent.net/'
},
{
author: 'Nihiltarier',
contact: 'arachi@airmail.cc',
langs: ['en'],
title: 'cadmican',
url: 'https://cadmican.neocities.org/'
},
{
author: 'Jonathan Skjøtt',
contact: 'jonathan@jskjott.com',
langs: ['en'],
title: 'jskjott',
url: 'https://jskjott.com'
},
{
author: 'sixeyes',
contact: 'elmusho@gmail.com',
langs: ['en'],
title: 'sixey.es',
type: 'portfolio',
url: 'https://sixey.es/'
},
{
author: '0xdstn',
contact: '0xdstn@protonmail.com',
langs: ['en'],
title: '0xdstn',
type: 'wiki',
url: 'https://tilde.town/~dustin/'
},
{
author: 'James',
contact: 'henderson.j@protonmail.com',
langs: ['en'],
rss: 'https://jameschip.io/index.xml',
title: 'James Chip',
url: 'https://jameschip.io/'
},
{
author: 'Patrick Monaghan',
contact: '0x5f.manpat@gmail.com',
langs: ['en'],
type: 'portfolio',
url: 'https://patrick-is.cool'
},
{
author: 'icyphox',
contact: 'x@icyphox.sh',
langs: ['en'],
rss: 'https://icyphox.sh/blog/feed.xml',
type: 'hybrid',
url: 'https://icyphox.sh'
},
{
author: 'roy niang',
contact: 'roy@royniang.com',
feed: 'https://telecom.royniang.com/updates.txt',
langs: ['en'],
title: 'roy niang',
type: 'portfolio',
url: 'https://royniang.com',
wiki: 'https://telecom.royniang.com/usefulnotes.ndtl'
},
{
author: 'raul altosaar',
contact: 'raul.altosaar@gmail.com',
langs: ['en'],
title: 'raul altosaar',
type: 'portfolio',
url: 'https://www.raul.earth/'
},
{
author: 'Cristi Constantin',
langs: ['en'],
rss: 'https://crlf.site/feed.xml',
title: 'Cr;Lf;',
type: 'hybrid',
url: 'https://crlf.site'
},
{
contact: 'johannesg@johannesg.com',
langs: ['en'],
title: 'Jóhannes Gunnar Þorsteinsson',
type: 'hybrid',
url: 'https://www.johannesg.com'
},
{
author: 'Michael Rupert',
contact: 'michaelrupert@fastmail.com',
langs: ['en'],
title: 'Provoke Analog',
type: 'hybrid',
url: 'https://provokeanalog.com'
},
{
author: 'eti',
contact: 'eti@eti.tf',
langs: ['en'],
url: 'https://eti.tf'
},
{
author: 'rezmason',
contact: 'jeremysachs@rezmason.net',
langs: ['en'],
title: 'rezmason.net',
type: 'hybrid',
url: 'https://rezmason.net'
},
{
contact: 'email@estfyr.net',
langs: ['en', 'cz'],
title: 'estfyr.net',
type: 'hybrid',
url: 'https://estfyr.net'
},
{
author: 'Payson Wallach',
contact: 'payson@paysonwallach.com',
langs: ['en'],
url: 'https://paysonwallach.com'
},
{
author: 'icco',
contact: 'nat@natwelch.com',
langs: ['en'],
type: 'portfolio',
url: 'https://natwelch.com'
},
{
author: 'whtrbt',
contact: 'pi@benjamin.gray.land',
langs: ['en'],
title: 'Park Imminent',
type: 'hybrid',
url: 'https://parkimminent.com'
},
{
author: 'aklsh',
contact: 'akileshkannan@gmail.com',
langs: ['en'],
url: 'https://aklsh.github.io',
type: 'hybrid'
},
{
author: 'hxii',
contact: 'paul@glushak.net',
langs: ['en'],
type: 'hybrid',
url: 'https://paulglushak.com'
},
{
author: 'syx',
contact: 'hello@simone.computer',
langs: ['en', 'ita'],
title: 'Simone\'s Computer',
type: 'portfolio',
url: 'https://simone.computer'
},
{
author: 'xj9',
contact: 'xj9@sunshinegardens.org',
feed: 'https://xj9.io/.well-known/webring/tw.txt',
langs: ['en'],
title: 'dreamspace',
type: 'blog',
rss: 'https://xj9.io/rss.xml',
url: 'https://xj9.io',
wiki: 'https://xj9.io/.well-known/webring/wiki.ndtl'
},
{
contact: 'joe.jenett@jenett.org',
langs: ['en'],
rss: 'https://simply.personal.jenett.org/feed',
title: 'jenett. simply. personal.',
type: 'blog',
url: 'https://simply.personal.jenett.org'
},
{
author: 'Wally',
contact: 'qpfiffer+xxiivvwebring@gmail.com',
langs: ['en'],
rss: 'http://q.pfiffer.org/feed.xml',
type: 'blog',
url: 'http://q.pfiffer.org/'
},
{
author: 'ashpex',
contact: 'ashpex@dismail.de',
langs: ['en'],
title: 'ashpex',
type: 'blog',
url: 'https://ashpex.neocities.org'
},
{
author: 'zvava',
contact: 'notminin@pm.me',
langs: ['en'],
title: 'zvava',
type: 'blog',
url: 'https://zvava.org/'
},
{
author: 'Amorphous',
contact: 'dl.amorphous@gmail.com',
langs: ['en'],
url: 'https://amorphic.space'
},
{
author: 'Edwin Wenink',
contact: 'edwinwenink@hotmail.com',
langs: ['en', 'nl'],
rss: 'https://www.edwinwenink.xyz/index.xml',
title: 'Archive Fever',
type: 'blog',
url: 'https://www.edwinwenink.xyz/'
},
{
author: 'Anne-Laure Le Cunff',
contact: 'alecunff@gmail.com',
langs: ['en', 'fr'],
rss: 'https://www.mentalnodes.com/sitemap.xml',
title: 'Mental Nodes',
type: 'hybrid',
url: 'https://www.mentalnodes.com'
},
{
author: 'librenauta',
contact: 'librenauta@riseup.net',
langs: ['es'],
title: 'copiona',
type: 'blog, hybrid',
url: 'https://copiona.com'
}
]
/*
Make sure you've read the README!
Don't forget the comma on the previous entry!
No trailing slashes or commas!
Alphabetize your keys!
protocol://url.domain.ext
*/
if (module && module.exports) {
module.exports = sites
}

View File

@ -1,176 +0,0 @@
'use strict'
const Wiki = sites => {
const entries = {}
const main = document.getElementById('main')
const aside = document.getElementById('aside')
const progress = document.getElementById('progress')
const progressCounter = Progress(sites.map(site => site.wiki))
const authors = new Set()
const terms = new Set()
const encodeUrl = text => text.replace(/ /g, '+').trim().toLowerCase()
const cleanEntry = entry => {
const temp = document.createElement('div')
temp.textContent = entry
return temp.innerHTML
}
const storeEntry = (author, url, category, term, unCleanEntry) => {
const entry = Array.isArray(unCleanEntry)
? unCleanEntry.map(x => cleanEntry(x))
: cleanEntry(unCleanEntry)
if (!(category in entries)) {
entries[category] = {}
entries[category][term] = [{ entry, authors: [{ author, url }] }]
} else if (!(term in entries[category])) {
entries[category][term] = [{ entry, authors: [{ author, url }] }]
} else {
const possibleIndex = entries[category][term].findIndex(existing => existing.entry === entry)
possibleIndex > -1
? entries[category][term][possibleIndex].authors.push({ author, url })
: entries[category][term].push({ entry, authors: [{ author, url }] })
}
terms.add(term)
authors.add(author)
}
const storeEntries = (author, url, category, entries) => {
Array.isArray(entries)
? storeEntry(author, url, category, `${category}-list-definition`, entries)
: Object.keys(entries).forEach(entry => storeEntry(author, url, category, entry, entries[entry]))
}
const transform = (author, url, ndtl) => {
Object.keys(ndtl).forEach(category => storeEntries(author, url, category, ndtl[category]))
}
const parse = (site, content) => {
console.log('Wiki', 'Parsing ' + site.wiki)
const ndtl = indental(content)
transform(site.author, site.url, ndtl)
}
const selected = (key, category) => key.toLowerCase() === category.toLowerCase()
? 'selected'
: ''
const randomTerm = () => {
const keys = Object.keys(entries)
return keys[Math.floor(Math.random() * keys.length)]
}
const formatSideBarCat = (key, entries) => (currentHtml, cat) => {
const catLength = Object.keys(entries[cat]).length
const newHtml = `<li class='${selected(key, cat)}'}'>
<a href='#${encodeUrl(cat)}' data-msgs='${catLength}'>${cat.toLowerCase()}</a>
</li>`
return `${currentHtml}${newHtml}`
}
const formatLinks = text => text.replace(
/\b((https?|dat):\/\/[^"\s]+)/g,
'<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>'
)
const formatAuthor = (authors, author) => {
return `${authors}<a class='author' target='_blank' href='${author.url}'> @${author.author}</a>`
}
const formatEntry = (definitions, definition) => {
const formatEntryList = entryList => entryList.reduce((items, item) => `${items}<li>${formatLinks(item)}</li>`, '')
const newHtml = typeof definition.entry === 'string'
? `<li>${formatLinks(definition.entry)}
- ${definition.authors.reduce(formatAuthor, '')}
</li>`
: `<li><ul>${formatEntryList(definition.entry)}
- ${definition.authors.reduce(formatAuthor, '')}
</ul></li>`
return `${definitions}${newHtml}`
}
const formatEntries = entries => (currentHtml, category) => {
const definitions = entries[category].reduce(formatEntry, '')
const title = category.endsWith('list-definition')
? ''
: `<li class='name'><strong>${category.toLowerCase()}</strong></li>`
return `${currentHtml}<ul class='term'>${title}${definitions}</ul>`
}
const findRelated = key => {
const searchOtherCategories = entry => {
return Object.keys(entries)
.filter(category => Object.keys(entries[category]).some(otherCat => otherCat === entry))
}
const foundMatches = Object.keys(entries[key])
.filter(entry => !entry.endsWith('list-definition'))
.map(searchOtherCategories)
const flattened = [].concat.apply([], foundMatches)
return Array.from(new Set(flattened)).filter(related => related !== key)
}
const formatRelated = (allRelated, relatedCategory) => {
const newHtml = `<span><a href='#${encodeUrl(relatedCategory)}'>/${relatedCategory.toLowerCase()}</a> </span>`
return `${allRelated}${newHtml}`
}
const refresh = key => {
if (key) {
if (entries[key] === undefined) return
const items = Object.keys(entries[key]).sort().reduce(formatEntries(entries[key]), '')
const relatedCategories = findRelated(key)
const relatedHtml = relatedCategories.length > 0
? relatedCategories.reduce(formatRelated, 'Related categories: ')
: ''
main.innerHTML = `<div class='related'><h1>${key}</h1>${relatedHtml}</div>${items}`
} else {
const html = `Click a /topic to get started, or try a <a href='#${randomTerm()}'>random page</a>`
const stats = `The wiki contains ${terms.size} terms in ${Object.keys(entries).length} categories, from ${authors.size} authors.`
main.innerHTML = `<h1>WIKI</h1>${html}<br />${stats}`
}
const homeCat = `<li class='${selected(key, '')}'>
<a href='#' data-msgs='${terms.size}'>home</a>
</li>`
const cats = Object.keys(entries).sort().reduce(formatSideBarCat(key, entries), homeCat)
aside.innerHTML = `<ul>${cats}</ul>`
}
const refreshProgress = () => {
progress.innerHTML = `${progressCounter}`
}
return {
initialize: hash => {
console.log('Wiki', 'Fetching...')
refreshProgress()
sites.filter(site => site.wiki && site.author).forEach(site => {
fetch(site.wiki, { cache: 'no-store' }).then(x => x.text()).then(content => {
parse(site, content)
refresh(stripHash(hash))
progressCounter.success(site.wiki).then(refreshProgress)
}).catch((err) => {
console.warn(`${site.wiki}`, err)
progressCounter.failure(site.wiki).then(refreshProgress)
})
})
},
refresh: key => { refresh(key) }
}
}
const decodeUrl = text => text.replace(/\+/g, ' ').trim().toUpperCase()
const stripHash = hash => {
const decoded = decodeURIComponent(hash)
return decoded.charAt(0) === '#' ? decodeUrl(decoded.substring(1)) : decoded
}
window.addEventListener('hashchange', () => {
wiki.refresh(stripHash(window.location.hash))
})

View File

@ -1,22 +0,0 @@
'use strict'
// extract twtxt feeds from sites.js
const fs = require('fs')
const sites = require('../scripts/sites.js')
const record = { following: {} }
for (const site of sites) {
if (!site.author || !site.feed) { continue }
record.following[site.author] = site.feed
}
const json = JSON.stringify(record)
fs.writeFile('twtxt.json', json, 'utf8', (err) => {
if (err) {
console.log('An error occured while writing JSON Object to File.')
return console.log(err)
}
console.log('Done.')
})

View File

@ -1 +0,0 @@
{"following":{"neauoire":"https://wiki.xxiivv.com/twtxt.txt","rho":"https://electro.pizza/twtxt.txt","joshavanier":"https://avanier.now.sh/tw.txt","kaemura":"https://kaemura.com/twttxt.txt","slisne":"https://liamcooke.com/merveilles/tw.txt","xvw":"https://xvw.github.io/twtxt/hallway.txt","lectronice":"https://lectronice.com/hallway/twtxt.txt","cblgh":"https://cblgh.org/twtxt.txt","jda0":"https://2d4.dev/tw.txt","wakest":"https://wake.st/twtxt.txt","drisc":"https://drisc.io/hallway/twtxt.txt","setphen":"https://phse.net/twtxt/merv.txt","kodedninja":"https://t.seed.hex22.org/twtxt.txt","m":"https://mboxed.github.io/sodatsu/tw.txt","tomupom":"https://a.tom.org.nz/twtxt.txt","amorris":"https://feed.amorris.ca/hallway.txt","stuartpb":"https://twtxt.stuartpb.com/xxiivv.txt","serocell":"https://xxiii.co/twtxt.txt","kormyen":"https://kor.nz/twtxt.txt","quite":"https://lublin.se/twtxt.txt","eli_oat":"https://txt.eli.li/twtxt/twtxt.txt","azlen":"https://azlen.me/twtxt.txt","vega":"https://opinionatedguide.github.io/twtxt.txt","cmaughan":"https://chrismaughan.com/twtxt.txt","Fundor 333":"https://fundor333.com/twtxt.txt","dcb":"https://dotcomboom.somnolescent.net/twtxt.txt","roy niang":"https://telecom.royniang.com/updates.txt","xj9":"https://xj9.io/.well-known/webring/tw.txt"}}

View File

@ -1,33 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="links/fonts.css"/>
<link rel="stylesheet" type="text/css" href="links/main.css"/>
<script type="text/javascript">const module = { exports:{} }</script>
<script src="scripts/progress.js"></script>
<script src="scripts/wiki.js"></script>
<script src="scripts/sites.js"></script>
<script src="scripts/indental.js"></script>
<script src="scripts/opml.js"></script>
<title>Wiki</title>
</head>
<body>
<div id="wiki">
<main>
<div id="main"></div>
<aside id="aside"></aside>
</main>
<footer id="footer">
<p>The <strong>Wiki</strong> is a decentralized encyclopedia, to join the conversation, add a <a href="https://github.com/XXIIVV/webring#joining-the-wiki">wiki:</a> field to your entry in the <a href="https://github.com/XXIIVV/Webring/">webring</a>.</p>
<p class='buttons'><a href='https://github.com/XXIIVV/webring'>Information</a> | <a href="hallway.html">The Hallway</a> | <a href="index.html">The Portal</a> | <a id="opml">OPML</a></p>
<div id="progress"></div>
</footer>
</div>
<script>
const wiki = Wiki(sites)
wiki.initialize(window.location.hash)
</script>
</body>
</html>